Code in React: Folder Structure That Scales

#code in react
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

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:

StrategyWhat it looks likeWhat it optimizes forWhere it breaks first
Layer-first/components, /hooks, /utils, /servicesEasy to start, easy to find “a component”“Shared” becomes a dumping ground, features become scattered
Route-first/routes/orders, /routes/settingsWorks well with nested routersCross-route reuse becomes unclear, domain logic leaks
Feature-first (recommended)/features/billing, /features/invite-teamClear ownership, local reasoning, parallel workRequires 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.

A clean project folder tree for a React application showing top-level directories like app, routes, features, shared, entities, and testing, with short annotations for what belongs where.

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 checks
  • features/ 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)

FolderCan import fromShould not import fromWhy
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.ts is 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.ts
  • entities/invoice/model/money.ts
  • entities/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

ConcernRecommendationWhy it scales
Component filesPascalCase.tsx for React componentsMakes UI easy to spot in diffs
Non-component modulescamelCase.tsSeparates behavior modules from UI
Public exportsOnly via index.ts per moduleMakes module boundaries explicit
ImportsPrefer absolute aliases (not ../../..)Improves refactors and readability

A note on “barrel files”:

  • Use index.ts to 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.tsx composes:
    • 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.tsx as 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-imports to block features/* importing other features/*
  • ESLint import rules to prevent deep imports like features/x/ui/internal/Thing
  • A “public API only” convention (import from features/invite-team, not from features/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 an index.ts and 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.