Most React projects don’t fail because of a bad library choice. They fail because nobody enforced a folder structure, and eighteen months later every file imports from every other file. Fixing that later costs weeks. Setting rules up front costs an afternoon.
A structure that scales
Group by feature, not by file type. A folder full of forty “components” with no relationship to each other is harder to navigate than ten feature folders that each own their own components, hooks, and API calls.
src/
app/ # routing, providers, app shell
routes/
providers/
features/
auth/
components/
hooks/
api.ts
types.ts
index.ts
billing/
components/
hooks/
api.ts
types.ts
index.ts
shared/
components/ # generic, reusable UI (Button, Modal, Input)
hooks/ # generic hooks (useDebounce, useLocalStorage)
lib/ # fetch wrapper, date utils, formatting
types/
assets/
The dependency rules that actually matter
- Features never import from other features directly. If
billingneeds something fromauth, that logic belongs inshared, or you’re exposing the wrong boundary. - Shared code never imports from features. Dependencies flow one direction:
sharedtofeaturestoapp. Never the reverse. - Each feature exposes one public entry point. An
index.tsthat re-exports only what other parts of the app are allowed to touch. Everything else inside the folder is private. - No file over roughly 300 lines. Not a hard rule, but a strong signal that a component or hook is doing too much and needs splitting.
Where state actually lives
Keep state as close as possible to where it’s used. Local UI state stays in the component. State shared across a feature lives in that feature’s hooks. State shared across the whole app — auth session, theme, current user — is the only kind that belongs in a global store.
Enforce it, don’t just document it
- Use ESLint’s
import/no-restricted-pathsor a similar rule to physically block cross-feature imports at lint time, not just in a wiki page nobody reads. - Add a barrel file check so features can’t be imported from anywhere except their
index.ts. - Review folder structure in code review the same way you’d review logic — a misplaced file is technical debt just like a bad function.
When to break the rules
Small apps and prototypes don’t need this. If you have fewer than ten components and one developer, feature folders are overhead. Introduce structure when the second developer joins or the third feature ships — not before, not much after.
If your team is also experimenting with AI coding agents to speed up refactors like this one, see how to build a serious dev workflow around Claude — a clean folder structure makes agent-driven refactors far more reliable, since the agent can reason about boundaries instead of guessing.
Quick FAQ
Should I use absolute imports or relative imports?
Absolute imports (via tsconfig path aliases) once you have more than two or three folder levels. Relative imports past ../../../ are a readability tax with no upside.
Do I need a monorepo for this to work?
No. Feature-based structure works fine inside a single app. Monorepos help when multiple apps share code, which is a separate problem.
What about a “core” or “common” folder that keeps growing?
That’s a smell. If shared keeps absorbing feature-specific logic, it usually means a feature boundary was drawn in the wrong place — revisit it instead of adding more to shared.
Leave a Reply