React Architecture Decision Checklist
A checklist of the architectural decisions that shape every React project, ordered from foundational to specific. Use it when kicking off a new app, auditing an existing one, or justifying a stack choice to a team.
How to Use This Checklist
- Walk the list top-to-bottom - earlier decisions constrain later ones.
- For each row, write down your choice and your rationale - rationale-less choices drift.
- Mark any row you skip explicitly as "N/A" with a reason, so the next engineer knows you considered it.
- Revisit the list at every major milestone (MVP, v1, scaling crunch) - the right answer changes as the project grows.
Architecture Decisions
-
Framework: Next.js vs Vite vs Remix vs TanStack Start
- Next.js: choose for SSR/SSG, SEO-critical apps, or when you want a batteries-included full-stack setup with App Router and React Server Components.
- Vite: choose for pure SPAs, internal tools, or dashboards where fast dev HMR matters more than SSR.
- Remix: choose when you want web-fundamentals-first nested routing, loaders/actions, and progressive enhancement baked in.
- TanStack Start: choose when you want type-safe routing with SSR but more flexibility than Next.js.
-
Rendering Strategy: SSR vs SSG vs CSR vs ISR
- SSR: public-facing pages with per-request personalization or frequently changing data.
- SSG: marketing sites, docs, or blogs where content rarely changes and you want cheap CDN hosting.
- CSR: authenticated app shells behind a login where SEO doesn't matter.
- ISR: hybrid sites with mostly static content but occasional background revalidation (e.g., ecommerce catalogs).
-
Language: TypeScript vs JavaScript
- TypeScript: default for any team or non-trivial app; catches bugs at compile time and makes refactors safer.
- JavaScript: only for tiny prototypes, throwaway demos, or when contributors strictly lack TS familiarity.
-
Package Manager: pnpm vs npm vs yarn vs bun
- pnpm: monorepos and projects sensitive to disk space and install speed, thanks to its content-addressable store.
- npm: zero-config, ships with Node, maximum compatibility with tutorials and CI examples.
- yarn: existing yarn berry (PnP) setups or teams already bought into yarn workspaces.
- bun: greenfield projects prioritizing raw install/run speed and willing to accept a younger ecosystem.
-
Repo Structure: Monorepo (Turborepo/Nx) vs Polyrepo
- Monorepo: multiple apps sharing UI components, types, or utils (web + mobile + admin dashboard).
- Polyrepo: single app, small team, or strict ownership boundaries between services.
-
Routing: File-based vs React Router vs TanStack Router
- File-based (Next/TanStack): convention over configuration; routes map to filesystem, great for standardization.
- React Router: SPAs where you want full programmatic control and a mature, familiar API.
- TanStack Router: you need fully type-safe routes with search param validation and loader-based data fetching.
-
Client State Management: Zustand vs Redux Toolkit vs Jotai vs Context API
- Zustand: minimal boilerplate, good default for most apps that need shared state.
- Redux Toolkit: large apps with complex state machines, time-travel debugging, or existing Redux muscle memory.
- Jotai: state that's naturally atomic and derived - good for complex forms or graph-like state.
- Context API: truly global, rarely-changing values like theme or auth user; avoid for anything that updates often.
-
Server State / Data Fetching: TanStack Query vs SWR vs RTK Query vs Apollo
- TanStack Query: default for REST - caching, mutations, devtools, framework-agnostic.
- SWR: simpler API, lightweight, tightly integrated if you're in the Vercel ecosystem.
- RTK Query: already using Redux Toolkit and want data fetching in the same store.
- Apollo: GraphQL with normalized caching, optimistic updates, and subscriptions.
-
Styling Approach: Tailwind vs CSS Modules vs styled-components vs vanilla-extract
- Tailwind: fast iteration, consistent design tokens, pairs great with shadcn/ui.
- CSS Modules: you want scoped plain CSS with zero runtime and no new DSL to learn.
- styled-components: dynamic theming and co-located CSS-in-JS, accepting the runtime cost.
- vanilla-extract: type-safe, zero-runtime CSS-in-TS for design system authors.
-
Component Library: shadcn/ui vs MUI vs Chakra vs Radix + custom
- shadcn/ui: you want copy-pasted, fully owned components built on Radix + Tailwind.
- MUI: enterprise apps needing comprehensive, Material-style components out of the box.
- Chakra: accessible, themeable components with a friendly prop-based API.
- Radix + custom: you want headless, accessible primitives and a fully bespoke visual identity.
-
Forms: React Hook Form vs TanStack Form vs Formik
- React Hook Form: default choice - performant, uncontrolled, pairs well with Zod resolvers.
- TanStack Form: deeply type-safe with first-class async validation and field arrays.
- Formik: only for legacy projects already using it; generally not recommended for new work.
-
Schema Validation: Zod vs Valibot vs Yup
- Zod: de facto standard; great TS inference, broad ecosystem integration (RHF, tRPC, etc.).
- Valibot: bundle-size-sensitive apps - modular and tree-shakable with Zod-like ergonomics.
- Yup: existing Formik projects or if you prefer its async-first validation style.
-
API Layer: REST vs GraphQL vs tRPC
- REST: public APIs, maximum client compatibility, or simple CRUD needs.
- GraphQL: multiple clients with divergent data needs and complex nested queries.
- tRPC: full-stack TypeScript project where the API is only consumed by your own clients.
-
Authentication: Auth.js vs Clerk vs Auth0 vs custom
- Auth.js (NextAuth): open-source, self-hosted, many OAuth providers out of the box for Next.js.
- Clerk: drop-in UI, user management dashboards, and you're happy to pay for speed.
- Auth0: enterprise SSO, SAML, and compliance requirements.
- Custom: unusual auth flows or strict data residency requirements that preclude third parties.
-
Unit/Integration Testing: Vitest vs Jest
- Vitest: Vite-based projects and anyone wanting a faster, ESM-native runner with a Jest-compatible API.
- Jest: large existing test suites, Next.js without Vite, or tooling that specifically assumes Jest.
-
E2E Testing: Playwright vs Cypress
- Playwright: multi-browser coverage (WebKit/Firefox), parallelism, and modern DX - default recommendation.
- Cypress: heavy time-travel debugging and a richer UI for flaky test diagnosis.
-
Linting & Formatting: ESLint + Prettier vs Biome
- ESLint + Prettier: mature ecosystem with plugins for every framework and rule imaginable.
- Biome: greenfield projects wanting a single, fast Rust-based tool for both linting and formatting.
-
Folder Structure: Feature-based vs Type-based vs Atomic Design
- Feature-based: co-locate all code per domain feature - scales best as apps grow.
- Type-based (components/, hooks/, etc.): fine for small apps but painful past ~20 features.
- Atomic Design: design-system-heavy apps where reusable UI primitives are the primary concern.
-
Code Splitting Strategy: Route-level vs Component-level lazy loading
- Route-level: default; split at page boundaries for the best TTI gains with minimal effort.
- Component-level: specific heavy widgets (charts, rich editors, maps) that are conditionally rendered.
-
Error Monitoring: Sentry vs LogRocket vs Datadog
- Sentry: best-in-class error tracking with source maps, releases, and performance monitoring.
- LogRocket: you need session replay alongside errors to debug UX issues.
- Datadog: you're already standardized on it for backend observability and want a single pane.
-
Analytics: PostHog vs Google Analytics vs Plausible vs Mixpanel
- PostHog: product analytics with feature flags, session replay, and a self-hosting option.
- Google Analytics: free, ubiquitous, and sufficient for marketing/traffic metrics.
- Plausible: lightweight, privacy-first, cookie-less - good for EU compliance.
- Mixpanel: funnel and cohort analysis for product teams that live in event data.
-
CI/CD: GitHub Actions vs GitLab CI vs CircleCI
- GitHub Actions: default if your repo is on GitHub - deep integration and a huge marketplace.
- GitLab CI: your source is on GitLab; tight Auto DevOps integration.
- CircleCI: teams wanting powerful parallelism primitives and mature caching out of the box.
-
Hosting / Deployment: Vercel vs Netlify vs AWS vs Cloudflare Pages
- Vercel: Next.js apps - first-party support, edge runtime, preview deploys.
- Netlify: Jamstack sites, serverless functions, and a generous free tier.
- AWS: you need full control, custom VPCs, or already live in AWS.
- Cloudflare Pages: edge-first apps, aggressive pricing, and tight Workers integration.
-
Environment Variable Management: .env files vs Doppler vs AWS Secrets Manager
- .env files: simple projects with few secrets and trusted devs.
- Doppler: teams needing shared, audited secret sync across envs without AWS complexity.
- AWS Secrets Manager: AWS-native stacks with rotation, IAM policies, and compliance needs.
-
Internationalization (i18n): next-intl vs react-i18next vs Lingui
- next-intl: Next.js App Router with first-class RSC support.
- react-i18next: mature, framework-agnostic, huge plugin ecosystem.
- Lingui: compile-time message extraction and a smaller runtime, ICU-based.
-
Animation: Framer Motion vs React Spring vs CSS/Tailwind
- Framer Motion: declarative animations, gestures, and layout animations with minimal code.
- React Spring: physics-based animations and fine-grained control.
- CSS/Tailwind: simple transitions and hover effects - no JS runtime cost.
-
Icon System: Lucide vs Heroicons vs custom SVG sprites
- Lucide: largest modern icon set with tree-shakable React components.
- Heroicons: clean, Tailwind-aligned, pairs well with shadcn/ui.
- Custom SVG sprites: brand-specific iconography or strict bundle-size budgets.
-
Date/Time Handling: date-fns vs Day.js vs Luxon vs Temporal
- date-fns: tree-shakable functional utilities; default choice for most apps.
- Day.js: Moment-like API in a tiny bundle - good for quick migrations off Moment.
- Luxon: heavy timezone and calendaring needs, built on Intl.
- Temporal (polyfill): you want the future standard today and are okay shipping the polyfill.
-
Component Documentation: Storybook vs Ladle vs none
- Storybook: design system or shared component library consumed by multiple teams.
- Ladle: Vite-based, faster alternative for simpler component catalogs.
- None: early-stage products where components change daily and docs rot faster than they help.
-
Image Optimization: next/image vs Cloudinary vs custom (Sharp + CDN)
- next/image: Next.js projects - automatic resizing, lazy loading, and format negotiation.
- Cloudinary: you need on-the-fly transformations, CDN delivery, and work in non-Next frameworks.
- Custom (Sharp + CDN): full control, cost-sensitive at scale, or highly specific image pipelines.
Applying the Checklist in Order
- Foundational tier (1–5): framework, rendering, language, package manager, repo structure. Expensive to reverse - get them right or accept the cost of a future migration.
- Core stack tier (6–14): routing, state, data, styling, UI, forms, validation, API, auth. Changeable, but each swap costs weeks.
- Quality tier (15–19): testing, linting, folder structure, code splitting. Easy to add later - but cheap to set up now and painful to retrofit.
- Ops tier (20–24): monitoring, analytics, CI/CD, hosting, secrets. Required before production traffic; don't ship without them.
- Polish tier (25–30): i18n, animation, icons, dates, component docs, images. Add when the product demands it - premature adoption bloats the bundle.
Gotchas
- Picking tools in isolation - Choosing "the best state library" before knowing the framework or rendering strategy leads to mismatches (e.g., Redux with RSC). Fix: Walk the checklist top-to-bottom; earlier decisions constrain later ones.
- Copying FAANG stacks for a 5-person team - Enterprise tools (Nx, Auth0, Datadog) add complexity and cost without the problems they solve. Fix: Match tooling to team size and traffic - upgrade when you feel the pain.
- No written rationale - Stack choices without rationale get questioned every time a new hire joins and silently drift. Fix: Write a one-sentence "why" per decision in an ADR (architecture decision record) committed to the repo.
- Locking in on the API layer too early - Picking GraphQL or tRPC before knowing your client shape often causes a painful rewrite at v2. Fix: Start with REST for the first iteration; migrate only once data-fetching patterns are clear.
- Adding observability last - Sentry, analytics, and feature flags installed post-launch miss the first round of real user bugs. Fix: Include error monitoring and analytics in the first production deploy, even with a free tier.
- Styling decision flip-flops - Mixing Tailwind, CSS Modules, and styled-components in one repo destroys bundle size and consistency. Fix: Pick one primary styling approach and enforce it via lint rules.
- Skipping TypeScript "to move fast" - JS prototypes that succeed become TS migrations under deadline pressure. Fix: Start with TypeScript even on prototypes; strict mode can come later.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| ADRs (Architecture Decision Records) | You want a full audit trail with context, consequences, and superseding notes. | You just need a quick checklist for a small team. |
| RFC / Tech Design Docs | Decisions need cross-team review before commitment. | The decision is scoped to one team and one repo. |
| Stack-selection generators (e.g., create-t3-app) | You want a preset "good-enough" stack and don't need to justify each choice. | You have constraints the generator doesn't know about (compliance, existing infra). |
| AWS Well-Architected Framework | Cloud infra decisions dominate your architecture. | The product is mostly a frontend SPA with minimal backend. |
FAQs
Why should I walk the checklist top-to-bottom instead of cherry-picking decisions?
- Foundational decisions (framework, rendering, language) constrain every later choice.
- Picking a state library before a framework leads to mismatches like Redux with RSC.
- Order reflects reversibility cost - rows 1–5 are expensive to undo; rows 25–30 are cheap.
When should I reach for Next.js over Vite?
- SSR/SSG or SEO-critical pages: Next.js.
- Pure authenticated SPA or internal dashboard with no SEO: Vite.
- Next.js adds App Router + React Server Components out of the box; Vite gives you the fastest dev HMR and nothing else.
What's the difference between SSR, SSG, CSR, and ISR in one sentence each?
- SSR - HTML rendered per request on the server.
- SSG - HTML built once at deploy time and served from a CDN.
- CSR - browser renders everything from a JS bundle, server ships an empty shell.
- ISR - SSG with background revalidation on a timer or trigger.
Is TypeScript really worth it for small prototypes?
- Yes, start with TypeScript even on prototypes - strict mode can come later.
- JS prototypes that succeed become TS migrations under deadline pressure.
- The only honest exceptions are throwaway demos or contributors who strictly lack TS familiarity.
When does a monorepo become worth it over a polyrepo?
- Monorepo pays off when you have multiple apps sharing UI, types, or utils (web + mobile + admin).
- Polyrepo wins for a single app, small team, or strict service ownership boundaries.
- Turborepo/Nx add caching and task orchestration - don't adopt until you feel the pain of duplicated tooling.
Which client state library should I pick by default?
- Zustand - minimal boilerplate, sane default for most apps.
- Redux Toolkit - large apps with complex state machines or existing Redux muscle memory.
- Jotai - atomic/derived state for forms or graph-like state.
- Context API - truly global, rarely-changing values like theme or auth user only.
Why is TanStack Query the default for server state?
- Caching, mutations, devtools, and framework-agnostic - works with any REST API.
- SWR is simpler but less feature-rich; use it if you're already in the Vercel ecosystem.
- RTK Query only makes sense if you're already on Redux Toolkit.
- Apollo is for GraphQL with normalized caching.
How do I decide between REST, GraphQL, and tRPC?
- REST - public APIs, max compatibility, simple CRUD.
- GraphQL - multiple clients with divergent data needs and nested queries.
- tRPC - full-stack TypeScript where the API is consumed only by your own clients.
- Starting with REST and migrating later is almost always cheaper than picking GraphQL too early.
Which styling approach pairs best with shadcn/ui?
- Tailwind - shadcn/ui is built on Tailwind + Radix; that's the canonical pairing.
- CSS Modules and vanilla-extract work but give up the design token benefits.
- Never mix Tailwind with styled-components in one repo - bundle size and consistency suffer.
What's the gotcha with picking Context API for shared state?
- Context re-renders every consumer on every value change - fine for static values, brutal for frequently-updating ones.
- Use it for theme, auth user, locale - not counters, form state, or server data.
- Reach for Zustand or Jotai the moment the value updates more than once per user interaction.
When should I add Sentry, analytics, and feature flags?
- In the first production deploy - retrofitting observability misses the first round of real user bugs.
- Free tiers of Sentry and PostHog cover most small apps.
- Include them in your initial CI/CD pipeline so release tagging and source maps Just Work.
Why is picking the API layer too early risky?
- GraphQL and tRPC both encode client shape assumptions that often turn out wrong at v2.
- A REST-first MVP gives you room to see actual data-fetching patterns before committing.
- Migrate once - not twice - by deferring the decision until the client shape stabilizes.
Do I need Storybook on day one?
- No - skip component docs until you have a design system consumed by multiple teams.
- Early-stage products change components daily; docs rot faster than they help.
- Ladle is a faster, Vite-based alternative when you eventually want catalog-style docs.
What's the quickest sign I over-engineered the stack?
- You're running Nx, Auth0, Datadog, and a GraphQL gateway for a 5-person team.
- Enterprise tools add cost and complexity without the problems they solve for small teams.
- Match tooling to team size and traffic - upgrade when the pain shows up, not before.
What's the first thing I should do after finishing this checklist?
- Commit your answers as an ADR (architecture decision record) in the repo.
- Include rationale and a revisit date for each decision.
- Share with the team and revisit at every major milestone (MVP, v1, scaling crunch).
Related
- React Fundamentals Best Practices - Component-level rules that follow from the stack choices here.
- Component Decision Workflow - How to decide where a component lives and what it owns.
- Next.js Setup - Concrete setup steps once Next.js is picked.
- TypeScript + React - Type patterns for any of the stack choices above.