How I Structure Large-Scale React & Next.js Applications
A practical breakdown of the folder structure, data layer, and component boundaries that keep a growing Next.js app from turning into spaghetti.
The problem with most React folder structures
Most React projects start clean and end up as a junk drawer: a components folder with 80 files, a utils.ts that does everything, and nobody sure where a new feature is supposed to live. On a project like BD Commerce — multi-currency, multiple admin roles, POS and online sync — that kind of drift isn't a minor annoyance. It's a production risk.
Here's the structure I actually use and why each piece exists.
1. Feature-first, not type-first
Instead of grouping by type (components/, hooks/, utils/), I group by feature:
features/
inventory/
components/
hooks/
api.ts
types.ts
orders/
components/
hooks/
api.ts
types.ts
When a feature changes, everything related to it lives in one place. When a feature gets deprecated, you delete one folder instead of hunting across the codebase.
2. A dedicated, typed data layer
Every feature gets an api.ts that wraps fetch/query logic, and a types.ts that defines the shape of the data. Components never call fetch directly. This one rule alone eliminates a huge class of bugs where two components disagree about what shape an API response actually is.
3. Shared UI lives separately from feature UI
Anything reused across features — buttons, modals, form inputs — lives in a top-level components/ui folder, versioned like a mini design system. This is exactly what let us cut deploy time by 25%: new features stopped reinventing basic UI, they just imported it.
4. Server vs. client boundaries are explicit
With the Next.js App Router, deciding what's a Server Component vs a Client Component isn't a detail — it's an architectural decision. My rule of thumb: data-fetching and layout shells default to Server Components; anything with interactivity, state, or browser APIs is explicitly marked "use client" and pushed as far down the tree as possible. This keeps bundle size down without a fight.
5. Co-locate tests and stories with the feature
A test file two folders away from the component it tests gets stale fast. Keeping Component.tsx, Component.test.tsx, and Component.stories.tsx next to each other means nobody has an excuse to skip updating tests when the component changes.
What this actually buys you
- New engineers onboard faster because the folder structure tells a story
- Features can be added or removed without touching unrelated code
- Code review is faster because the diff shows exactly which feature changed
- Shared UI actually gets reused instead of duplicated
None of this is exotic. It's discipline, applied consistently, from the first commit.
Handling shared state across features
A scalable react application structure isn't just about folders — it's also about where cross-feature state lives. My rule: if only one feature needs a piece of state, it stays inside that feature folder. If two or more features genuinely need to share it, it gets promoted to a top-level store/ or context/ directory — but only after that need is proven, not preemptively.
This avoids the common trap of building a giant global store on day one "just in case," which usually just creates a second, less organized copy of your feature folders.
Naming conventions that scale with the team
As a next.js architecture best practices habit, I enforce consistent naming across the codebase: PascalCase for components, camelCase for hooks and utilities, and a fixed suffix pattern (.api.ts, .types.ts, .hooks.ts) so any engineer can predict a file's purpose from its name alone, without opening it. This sounds trivial until you're the fifth engineer joining a two-year-old codebase and trying to find where an API call lives.
Testing strategy tied to the same structure
Because tests are co-located with features, our testing pyramid naturally follows the architecture: unit tests for isolated hooks and utilities inside each feature folder, integration tests for feature-level flows, and a small set of end-to-end tests covering the critical paths — checkout, login, order sync — that would actually hurt the business if they broke.
The compounding payoff
None of these individual decisions look dramatic. But six months into a project, the difference between a team that followed this structure and one that didn't is stark: one team ships new features by importing existing pieces, and the other rewrites something similar from scratch every time, because nobody could find — or trust — what already existed.