Blog
The React Folder Structure Nobody Teaches Juniors
Why flat folders break down, what feature-based architecture actually looks like, and how to structure React apps that survive the first six months of real work.
Most React tutorials end the same way.
You get a src/ folder. You put components in components/. Pages in pages/. Maybe a utils/ folder for helpers you will definitely organize later. You build a todo app, a dashboard, maybe a CRUD form. Everything works. You feel productive.
Then you join a real team — or your side project grows — and the codebase starts feeling like a junk drawer.
You cannot find where API calls live. A button in the billing screen imports a hook from the auth folder. Three people name the same thing differently. Every pull request touches twelve unrelated files. Onboarding a new developer takes a week of archaeology.
Nobody sat you down and explained that folder structure is architecture. Not decoration. Not something you refactor “when you have time.” It is the difference between a codebase that scales and one that slowly teaches you to hate React.
This post is about the structure I wish someone had explained to me earlier — the feature-based approach popularized in guides like Bulletproof React, and why it works when the classic junior layout does not.
The layout everyone learns first
It looks reasonable:
src/
├── components/
├── pages/
├── hooks/
├── utils/
├── services/
└── App.tsx
For a week, this is fine.
The problem is not the folder names. The problem is what they imply: that your app is organized by technical type (components, hooks, utils) instead of by business capability (auth, billing, teams, settings).
So when you add a “Teams” feature, your code gets scattered:
components/TeamCard.tsxcomponents/TeamList.tsxhooks/useTeams.tsservices/teamApi.tsutils/formatTeamName.tspages/TeamsPage.tsx
Now imagine you have auth, comments, discussions, notifications, and an admin panel. Your components/ folder has 80 files. Good luck finding anything without global search and prayer.
This is the structure nobody warns you about — because tutorials never stay alive long enough to rot.
What juniors are actually missing: boundaries
Production React apps are not hard because JSX is hard. They are hard because change spreads.
You fix a bug in one screen and break another. You add a field to an API response and discover four components were parsing it differently. A shared hook starts carrying state for two features that only looked similar at first.
What you need is not more folders. You need boundaries — clear rules about what can depend on what.
The feature-based model gives you that.
The structure that scales
Instead of sorting files by file type across the whole app, you group code by feature — a slice of product behavior that a user (or another developer) can reason about as one unit.
At a high level, it looks like this:
src/
├── app/ # wiring: routes, providers, router
├── assets/ # global static files
├── components/ # truly shared UI (Button, Modal, Layout)
├── config/ # env, app-wide constants
├── features/ # where most of your code lives
├── hooks/ # shared hooks only
├── lib/ # preconfigured clients (axios, query client, etc.)
├── stores/ # global state (if you need it)
├── types/ # shared types
└── utils/ # generic helpers (formatDate, cn, etc.)
The important shift: features/ is the default home for product code, not components/.
Each feature is a mini-application inside your application:
src/features/discussions/
├── api/ # API calls + query/mutation hooks for this feature
├── components/ # UI used only inside discussions
├── hooks/ # hooks scoped to discussions
├── types/ # TypeScript types for this domain
├── utils/ # helpers that only discussions need
└── stores/ # local state if the feature needs it
You do not need every subfolder for every feature. A small feature might only have components/ and api/. That is the point — the structure grows with the feature, not ahead of it.
The Bulletproof React docs put it well: only include what the feature actually needs. Empty folders are ceremony. Boundaries are not.
The rule that changes everything: no cross-feature imports
This is the part most teams skip — and pay for later.
If features/billing imports from features/auth, you have created a hidden coupling. Billing now breaks when auth refactors. Your dependency graph becomes a plate of spaghetti and nobody can draw it on a whiteboard anymore.
The rule:
Features do not import from other features.
When two features need to interact, you compose them at the app layer — in routes, layouts, or orchestration components — not by reaching into each other’s folders.
Example: a “Team settings” page needs both team data and user permissions. Do not import useAuth from inside features/teams. Build the page in app/routes (or your pages directory) and compose hooks from both features there.
Some teams enforce this with ESLint import/no-restricted-paths — literally blocking illegal imports at lint time. That might feel strict until the first time it stops a Friday-night production bug.
Unidirectional flow: shared → features → app
There is a second rule that pairs well with the first, and Bulletproof React documents it clearly:
Code flows in one direction.
components/,hooks/,lib/,utils/,types/— shared layer. Anyone can use these.features/*— can import from shared. Cannot import fromapp/.app/— can import from features and shared. This is where routes assemble the product.
Shared code must never import from a feature. If your generic Button starts importing from features/checkout, your shared layer is lying about being shared.
This keeps mental models simple: open a feature folder and everything inside belongs to that domain. Open components/ui and you know it is generic.
What goes in shared components/ vs feature components/?
Juniors over-share. Everything lands in components/ because “we might reuse it.”
Senior rule of thumb:
Start in the feature. Move to shared when reuse is proven — not imagined.
That UserAvatar on the billing page? It lives in features/billing/components/ until a second feature actually needs it. Then you promote it — with a real API, not copy-paste.
Shared components/ should be your design system and layout primitives: buttons, inputs, dialogs, empty states, page shells. Not every card you ever built.
Bulletproof React also recommends wrapping third-party components (a Link wrapper around React Router, a styled Radix dialog) so you can swap libraries without rewriting every screen. That is boring infrastructure work — and it saves months later.
API layer: per-feature or global?
Two valid patterns:
Per-feature API (inside features/foo/api/): best when endpoints are mostly used by one domain. Keeps fetch logic next to the UI that consumes it.
Central api/ folder: better when many features share the same endpoints or you have a small app with heavy overlap.
Neither is morally superior. Pick one, document it, stay consistent. The worst choice is both — some calls in services/, some in features/auth/api, some inline in components because someone was in a hurry.
If you use React Query or similar, feature-scoped api/ folders often contain your query keys, fetchers, and hooks together. That colocation is underrated. When the backend changes the discussions endpoint, you should not grep half the repo.
The barrel file trap
Older React advice loved index.ts barrel files:
// features/discussions/index.ts
export * from './components';
export * from './api';
export * from './hooks';
Clean imports. Pretty diffs. Felt professional.
Then bundlers got smarter about tree-shaking — and barrels got dangerous. Export everything from one file and you risk pulling entire modules when you only wanted one hook. Vite and modern tooling can struggle with this depending on how you export.
Current best practice from the Bulletproof React guide: import directly from the file you need, not from a feature barrel. Slightly uglier imports. Healthier bundles. Fewer surprise circular dependencies.
Small trade. Worth it.
How this feels on a real project
Bulletproof React’s sample app is a team/discussion product — users, teams, threads, comments, roles. Classic multi-entity SaaS shape.
In a flat structure, AdminDeleteCommentButton.tsx sits somewhere in a 200-file components/ tree next to Logo.tsx and PricingTable.tsx.
In a feature structure:
- Comment deletion logic lives under
features/comments/ - Role checks live near the auth/permissions feature
- The route composes them
When product says “only team admins can delete comments in archived discussions,” you know where to look. You are not searching utils/ at 11pm.
That is the real payoff. Not aesthetic folders. Predictable change.
What to do if your app is already a mess
You do not rewrite everything on Monday.
- Stop the bleeding. New code goes into
features/. No new files in rootcomponents/unless they are genuinely shared UI. - Pick one feature — usually the one you are actively building — and move it wholesale. Accept that imports will be ugly for a sprint.
- Add one ESLint rule blocking cross-feature imports. Let the linter teach the team.
- Delete dead code as you move. Migrations are free cleanup if you are ruthless about unused exports.
Refactoring folder structure is not glamorous work. Neither is paying interest on a bad structure for two years.
What juniors should learn instead of memorizing patterns
Memorizing folder names is useless. Internalize these questions:
- Who owns this code? (Which feature?)
- Who is allowed to import it? (Shared, feature, or app layer?)
- What happens when this feature doubles in size? (Does the folder still make sense?)
- Can I delete this feature without surgical repo-wide edits? (If no, boundaries failed.)
React gives you freedom. Freedom without constraints is how you get a 40-file components/ folder named things like FinalButton2.tsx.
Closing thought
Nobody teaches juniors folder structure because bootcamps optimize for demos, not maintenance. A todo app does not need features/. A product with auth, billing, teams, and an admin panel absolutely does.
The feature-based layout is not the only valid architecture. Monorepos, domain-driven design, micro-frontends — all have their place. But if you are a solo dev or small team shipping a React product, the model in Bulletproof React is one of the clearest starting points I have seen: colocate by feature, enforce boundaries, compose at the app layer, promote to shared only when reuse is real.
Your future self — the one debugging a production issue at midnight — will not thank you for a perfect utils/ folder.
They will thank you for knowing exactly which folder to open.