Code in React: Folder Structure That Scales

A folder structure is not “just aesthetics”. It is how you encode boundaries, ownership, and change safety into your codebase. If your team is shipping more than a couple of features per month, the difference between “works” and “scales” shows up fast: longer PRs, mysterious regressions, circular imports, and a growing fear of touching “shared” code.
This guide gives you a production-minded React folder structure that scales, plus the rules that make it work. It is written for engineers and tech leads who want to keep code in React maintainable as the app and the team grows.
What “scalable” means for a React codebase
A scalable structure is one that still feels predictable after:
- You add 20+ screens and multiple user journeys
- You introduce permissions, feature flags, and multiple layouts
- You onboard new developers without tribal knowledge
- You split the work across teams (or at least across clear areas of ownership)
The goal is not to find the one true layout. The goal is to make change cheap.
The 3 folder structures that teams typically try (and why one wins)
Most React repos start with one of these organizing axes:
| Strategy | What it looks like | What it optimizes for | Where it breaks first |
|---|---|---|---|
| Layer-first | /components, /hooks, /utils, /services | Easy to start, easy to find “a component” | “Shared” becomes a dumping ground, features become scattered |
| Route-first | /routes/orders, /routes/settings | Works well with nested routers | Cross-route reuse becomes unclear, domain logic leaks |
| Feature-first (recommended) | /features/billing, /features/invite-team | Clear ownership, local reasoning, parallel work | Requires explicit dependency rules to prevent messy sharing |
For most product teams, feature-first scales best because it matches how you ship: you ship features, not “components”. You can still have shared UI and shared infrastructure, but they must be intentionally constrained.
A folder structure template that scales
Here is a pragmatic structure that works for React SPAs (Vite + React Router) and can be adapted to meta-frameworks (Next.js, Remix). The names matter less than the separation.

Recommended top-level layout
src/
app/
providers/
layout/
config/
bootstrap.tsx
routes/
_shell/
home/
settings/
features/
invite-team/
api/
model/
ui/
index.ts
billing/
api/
model/
ui/
index.ts
entities/
user/
model/
api/
index.ts
organization/
model/
api/
index.ts
shared/
ui/
lib/
api/
config/
test/
factories/
msw/
types/
This structure enforces a simple idea:
routes/orchestrates: page composition, loaders, route-level error boundaries, access checksfeatures/ships work: user-facing capabilities (Invite Team, Billing, Export CSV)entities/holds domain building blocks: User, Organization, Invoice (types, invariants, domain helpers)shared/is boring infrastructure: generic UI primitives and libraries without business meaning
If you already have a Next.js App Router repo, the equivalent is to keep your app/ routes as the orchestration layer, while features/, entities/, and shared/ remain framework-agnostic.
What goes where (with rules you can enforce)
A scalable structure needs rules that people can repeat under pressure. The easiest way to make this actionable is to define “allowed dependencies”.
Dependency rules (the part most teams skip)
| Folder | Can import from | Should not import from | Why |
|---|---|---|---|
shared/ | Only shared/ | features/, routes/, entities/ | Keeps the shared layer truly reusable |
entities/ | shared/, other entities/ (carefully) | features/, routes/ | Prevents domain concepts from depending on UI workflows |
features/ | shared/, entities/ | Other features/ (by default) | Avoids “feature spaghetti” and hidden coupling |
routes/ | features/, entities/, shared/ | Nothing (it is the top) | Routes compose the app, they are allowed to know more |
Default rule: features should not import from other features.
When you feel the urge to do it, you usually discovered one of these:
- A missing domain concept (promote to
entities/) - A genuinely reusable UI primitive (promote to
shared/ui/) - A workflow that is actually a product area (merge features or define a clearer boundary)
What lives inside a feature/
Keep feature internals boring and consistent. This template works well:
features/invite-team/
api/
inviteTeam.ts
useInviteTeamMutation.ts
model/
schemas.ts
types.ts
ui/
InviteTeamDialog.tsx
InviteTeamForm.tsx
index.ts
Guidelines:
api/contains networking and server-state integration (fetchers, query hooks, mutation hooks).model/contains validation, types, and business rules for the feature (often Zod schemas plus derived types).ui/contains components that are meaningful in the feature context.index.tsis the only public surface (export the minimal things the route needs).
This turns each feature into a small module you can reason about, test, and refactor without scanning the entire repo.
What belongs in entities/
Entities are your domain nouns. They are not “models” in the MVC sense, they are stable concepts that multiple features might reference.
Examples that fit well:
entities/user/model/permissions.tsentities/invoice/model/money.tsentities/organization/api/getOrganization.ts
Counterexamples (keep in features/):
- “InviteTeamDialog state machine” (feature workflow)
- “Billing page filters and URL state” (route + feature orchestration)
File naming and export conventions that prevent churn
Folder structure helps, but scale failures often come from inconsistent exports and naming. Pick defaults that make refactors cheap.
A practical convention set
| Concern | Recommendation | Why it scales |
|---|---|---|
| Component files | PascalCase.tsx for React components | Makes UI easy to spot in diffs |
| Non-component modules | camelCase.ts | Separates behavior modules from UI |
| Public exports | Only via index.ts per module | Makes module boundaries explicit |
| Imports | Prefer absolute aliases (not ../../..) | Improves refactors and readability |
A note on “barrel files”:
- Use
index.tsto define the public API of a feature or entity. - Avoid deep barrel patterns that re-export half the repo. They obscure dependencies and can slow builds.
Routing as an orchestration seam (React Router and Next.js)
A common mistake is to let routes contain domain logic, data shaping, and UI details. A scalable approach is the opposite: routes compose.
React Router example
routes/settings/route.tsxcomposes:- access checks
- data loading
- layout
- feature components
It should not contain:
- low-level fetch calls
- schema validation logic
- complex transformation logic that should be tested in isolation
Next.js App Router adaptation
In Next.js, the route seam is your app/.../page.tsx (and sometimes layout.tsx). The same principle applies:
- Keep
page.tsxas a composition layer. - Put feature logic in
features/...and import it. - Keep shared utilities in
shared/.
This avoids the slow drift where your app/ directory becomes your entire application.
Shared UI: the fastest way to create a “junk drawer”
Every team eventually creates components/ and starts adding everything there.
A scalable shared UI layer is intentionally limited:
shared/ui/contains primitives with no business meaning (Button, Modal, Tabs).- Feature components stay in
features/<name>/ui/even if they are reused once or twice.
A good test is naming:
- If it is named after a business concept (InvoiceStatusBadge), it is not shared UI.
- If it is a primitive (Badge), it can be.
This prevents the “shared component tax” where changing one UI component unexpectedly breaks five unrelated screens.
Data access: structure is a safety mechanism
Scaling React apps usually means scaling server state. Regardless of whether you use TanStack Query, RTK Query, SWR, or framework loaders, the organization matters.
Practical defaults:
- Put feature-specific query hooks inside the feature (
features/billing/api/useInvoicesQuery.ts). - Put entity-level fetchers and types in entities (
entities/user/api/getUser.ts). - Put the low-level HTTP client in shared (
shared/api/httpClient.ts).
This setup makes dependency direction obvious: shared client, entity fetchers, feature hooks, route composition.
Tooling that enforces the structure (so it survives reality)
A folder structure that depends on discipline will fail during deadlines. Enforce rules in CI.
High-signal guardrails:
- TypeScript path aliases (for readable imports and easier moves)
- ESLint
no-restricted-importsto blockfeatures/*importing otherfeatures/* - ESLint import rules to prevent deep imports like
features/x/ui/internal/Thing - A “public API only” convention (import from
features/invite-team, not fromfeatures/invite-team/ui/...)
These are not expensive to add, and they pay for themselves the first time they stop an accidental circular dependency.
A migration plan for an existing messy React repo
You do not need a rewrite. A folder reset can be incremental.
A safe sequence:
- Start by introducing
shared/and moving true primitives there (Button, Modal,formatDate). - Pick one high-change area and turn it into a feature module (
features/invite-team). Add anindex.tsand only import through it. - Add an ESLint restriction that prevents new cross-feature imports, even if you still have some legacy ones.
- Promote stable nouns into
entities/only when two or more features actually need them. - Keep routes as orchestration. Each time you touch a route, push logic down into features/entities.
If you do this over a few sprints, the structure “wins by gravity” and the old layout stops growing.
Why this matters outside software teams too
Even in non-software industries, maintainable UI code becomes an operational advantage. Consider companies that sell and install physical infrastructure, like a provider of photovoltaic systems, backup power, and electrical installations. They often end up needing internal portals for quoting, scheduling, maintenance, and customer communication. If you are building that kind of React app, a feature-first structure makes it easier to grow from “one dashboard” into a multi-workflow system. As a real-world example of such a business context, see Notstrom & Elektrotechnik Sven Sanny.
When to bring in outside help
If you are already feeling the pain (slow onboarding, fragile shared code, PRs that touch 30 files across unrelated areas), the fastest fix is usually a short architecture and codebase review focused on:
- module boundaries and dependency direction
- refactoring plan with low-risk fracture planes
- a minimal set of enforced rules (lint, CI gates)
Wolf-Tech provides full-stack development and code quality consulting for teams that need to modernize or scale existing React codebases. If you want a second set of senior eyes on your structure before it hardens into legacy, you can explore options at wolf-tech.io.

