Opinionated guide to scalable React architecture, folder strategy, state boundaries, testing, and DX.
If I were setting a React standard for a team today, I would not chase the most academic folder structure. I would optimize for discoverability, predictable boundaries, route-level performance, semantic UI, and tests that protect real product behavior.
- Frontend Architecture Playbook
- Table of Contents
- Why this exists
- Companion playbooks
- The defaults I'd reach for first
- The two structures worth knowing
- My recommended default: the hybrid model
- Shared modules structure
- Feature or domain structure
- Component and hook anatomy
- Performance defaults
- API contract, types, and errors
- Authentication and sessions (Node API)
- Testing defaults
- Semantic HTML and accessibility habits
- Environment and secrets
- Suggested repository structure
- References and inspiration
- License
React projects usually become messy in a very predictable way.
They begin tidy, then slowly turn into:
- one
components/folder nobody wants to open; - pages that know too much;
- utilities that quietly became infrastructure;
- hooks with half the app inside them;
- tests that lag behind the product;
- performance fixes added late, one route at a time.
The raw notes in this repository already point in the right direction. This README turns them into a clearer standard you can publish, share, and actually enforce.
These repositories form one playbook suite:
- Auth & Identity Playbook — sessions, tokens, OAuth, and identity boundaries across the stack
- Backend Architecture Playbook — APIs, boundaries, OpenAPI, persistence, and errors
- Best of JavaScript — curated JS/TS tooling and stack defaults
- Caching Playbook — HTTP, CDN, and application caches; consistency and invalidation
- Code Review Playbook — PR quality, ownership, and review culture
- DevOps Delivery Playbook — CI/CD, environments, rollout safety, and observability
- Engineering Lead Playbook — standards, RFCs, and technical leadership habits
- Frontend Architecture Playbook — React structure, performance, and consuming API contracts
- Marketing and SEO Playbook — growth, SEO, experimentation, and marketing surfaces
- Monorepo Architecture Playbook — workspaces, package boundaries, and shared code at scale
- Node.js Runtime & Performance Playbook — event loop, streams, memory, and production Node performance
- Testing Strategy Playbook — unit, integration, contract, E2E, and CI-friendly test layers
If I were starting a modern React application today, I would usually default to this:
- Structure: feature-first for product code, shared folders for true cross-app primitives
- Routing: code-split at the route level by default
- Components: colocate component, test, story, styles, and re-export index when the component matters enough
- Hooks: keep reusable hooks small, named, and colocated with their tests if they contain real logic
- State: colocate feature state with the feature whenever possible; use TanStack Query for server state so caching, retries, and error boundaries stay consistent with your API
- Types: prefer types generated from the same OpenAPI the backend owns (
openapi-typescript, Hey API, Orval — see Best of JS); avoid duplicating request/response shapes by hand - Errors: parse API error bodies with the same JSON fields the backend documents (
code,message,request_id); surfacerequest_idin toasts or error UIs when users report bugs - Testing: Vitest + Testing Library for unit and component tests; Jest only where legacy already dictates it
- Analytics: important instrumentation should be testable, not "fire and pray"
- Markup: prefer semantic HTML before div soup
- Secrets:
.envfiles are for runtime wiring, not for committing secrets into Git history
The source notes highlight two major ways to organize a React codebase.
This is the familiar style:
components/hooks/utils/config/pages/routes/store/
It is easy to start with and easy for new developers to recognize.
This style pushes product code into feature areas like:
modules/dashboardmodules/detailsmodules/common
It scales better when the application grows sideways and multiple product areas evolve independently.
Both structures are valid. The real question is not "which one is correct?" The question is "which one fails later?"
This is the model I would recommend to most teams.
- pages with real business behavior;
- feature-specific components;
- state slices;
- API adapters tied to a single product area;
- selectors, hooks, and tests that mostly serve one domain.
- design-system primitives;
- app shell concerns;
- routing bootstrap;
- global config;
- localization;
- truly reusable hooks and utilities;
- cross-feature types and constants.
That gives you a healthier balance:
- not every button becomes its own "domain";
- not every domain gets flattened into a giant global
components/folder.
This structure works well for smaller apps, design-system repos, or codebases that are still mostly UI composition.
src/
assets/
components/
Button/
Button.tsx
Button.test.tsx
Button.styles.ts
Button.stories.tsx
index.ts
pages/
routes/
store/
rootReducer.ts
hooks/
useSomeStuff/
useSomeStuff.ts
useSomeStuff.test.ts
index.ts
utils/
validation/
config/
locales/
types/
constants/
App.tsx
main.tsx- clear at the beginning;
- low conceptual overhead;
- easy for shared UI work.
- feature logic gets scattered across many top-level folders;
- product changes require editing files in five places;
- ownership becomes fuzzy.
This is usually the stronger long-term choice for application code.
src/
app/
router/
providers/
store/
modules/
common/
components/
Button/
Input/
dashboard/
api/
components/
Table/
Sidebar/
hooks/
routes/
state/
tests/
details/
api/
components/
hooks/
routes/
state/
tests/
shared/
assets/
config/
hooks/
lib/
locales/
types/
main.tsx- feature code stays together;
- refactors become local instead of repo-wide;
- testing becomes more natural;
- onboarding improves because the code mirrors the product.
This direction is also aligned with public guidance around feature grouping and colocating logic near the code that owns it.
The source notes already contain a useful instinct here: when a component or hook is important enough, give it a little home.
Button/
Button.tsx
Button.test.tsx
Button.styles.ts
Button.stories.tsx
index.tsuseSomeStuff/
useSomeStuff.ts
useSomeStuff.test.ts
index.ts- implementation;
- tests;
- storybook story if it is part of a reusable UI layer;
- styles if the styling approach benefits from colocation;
index.tsre-export to keep imports clean.
- component folders for trivial one-file throwaways;
- giant barrel exports that hide ownership;
- generic hooks that quietly depend on half the app.
One of the smartest notes in the source material is also one of the simplest:
code-split pages at the router level to improve load time
That should be the default in most product apps.
- lazy-load route chunks;
- keep dashboard-sized dependencies out of the initial bundle if the landing page does not need them;
- avoid dragging feature-only code into shared modules;
- profile before "optimizing" every component manually.
Route-level code splitting gives you one of the highest signal-to-effort wins in React architecture.
The backend playbook treats OpenAPI as the contract. The React app should consume that contract instead of guessing.
- Codegen: generate TypeScript types (and optionally clients) from the shared spec; wire outputs through a workspace package in a monorepo or a published
@your-scope/api-typespackage. - Data fetching: implement feature hooks with TanStack Query (
useQuery/useMutation) so loading and error states stay uniform; map HTTP status and parsed error JSON in one place (for example a sharedparseApiErrorhelper used byQueryClient's global handlers). - Stability: agree with the API on validation status codes (400 vs 422) and on the error envelope; UI code should not special-case different shapes per endpoint.
- Supportability: when the API returns
request_id, show it (or copy it) on error screens so logs and user reports match.
Browser auth should match how your Express / Fastify / Hono service issues and checks credentials (see the backend playbook's pipeline section).
- Cookies vs bearer tokens: if the API uses httpOnly cookies, prefer same-site patterns and a BFF or first-party proxy when the SPA and API are on different origins; if you use Authorization: Bearer, never store tokens in places that sync to Git or public bundles — treat refresh flows as part of the architecture, not a one-off fetch.
- CORS: configure allowed origins and credentials explicitly; "allow everything" is a common source of subtle production bugs.
- Alignment: the same OpenAPI document should describe security schemes (cookie, bearer, OAuth2) so generated clients and manual
fetchwrappers stay honest.
Secrets for third-party auth (OAuth client secrets, API keys) stay in vaults / CI / platform config, not in committed .env files — the environment section below still applies.
The README should make testing expectations obvious, not optional folklore.
- all new or modified logic should have unit test coverage using Vitest and Testing Library as the default stack for new work;
- Jest is acceptable when the repo already standardizes on it; do not mix two runners in the same package without a migration plan;
- important custom hooks should be tested if they contain branching or side effects;
- analytics events should be testable, ideally with explicit spies or mocks;
- unit tests should run on feature branches and again when merged to the main branch (see the DevOps playbook for lane layout).
Instrumentation code is easy to ignore because it rarely blocks local development. That is exactly why it drifts.
If analytics matters to the business, test it like product behavior.
Example idea:
import { vi } from "vitest"
vi.spyOn(analytics, "track")Then assert the event payload from the actual interaction path, not from a detached helper test.
Prefer semantic elements when they match the job.
<header><nav><main><section><article><aside><footer><figure><figcaption><ul><li><time datetime="...">
That does not make the app accessible by magic, but it gives the UI better structure for users, assistive tech, maintainers, and search engines.
A React codebase with strong architecture and weak semantics is still incomplete.
The .env note in the source material is exactly the right instinct.
- environment files are configuration, not a secret-management strategy;
- sensitive values should come from vaults, platform secrets, or CI-managed injection;
.envshould not become a graveyard of production secrets committed by accident.
If a repo needs runtime configuration, document the expected variables. If it needs secrets, use proper secret management.
This is the version I would use for a serious app:
src/
app/
providers/
router/
store/
modules/
auth/
dashboard/
details/
shared/
assets/
components/
config/
hooks/
lib/
locales/
types/
tests/
main.tsxAnd inside a real feature:
modules/
dashboard/
api/
components/
DashboardTable/
DashboardSidebar/
hooks/
routes/
state/
tests/
index.tsSimple enough to scan. Structured enough to scale.
- bulletproof-react: project structure
- Awesome React
- React + TypeScript clean architecture boilerplate
MIT is a sensible default for a guide repository like this, but choose the license that matches how open you want reuse to be.