I've watched folder structures collapse under their own weight—routes scattered everywhere, components duplicated across projects, and a simple refactor becoming a full-day nightmare. After shipping 200+ component applications across e-commerce, CRM, and SaaS platforms, I've landed on a folder structure for large Next.js apps that actually scales. This isn't about perfection; it's about predictability and speed when your codebase grows beyond what a flat structure can handle.
Why Folder Structure Matters at Scale
When you're building with five components, folder structure feels optional. When you're building with 200, it becomes your team's operating system. A poor structure forces developers to guess where things live, encourages duplication, and makes refactoring feel like defusing a bomb. I've been there—hunting for where a modal component lives because three versions exist in different places, each slightly different.
A deliberate folder structure does three things: it makes the codebase navigable without hunting, it prevents duplication by establishing clear boundaries, and it enables parallel work without constant conflicts. The structure I'll walk through is battle-tested across trading dashboards with real-time updates, ride-hailing apps with complex state, and multi-tenant SaaS platforms.
Feature Folders — The Heart of Organization
Feature folders group everything related to a business capability in one place. If you're building a leads management system, everything about leads—components, hooks, API actions, types, utilities—lives under features/leads. This is domain-driven design applied to the frontend.
Here's what I typically see inside a feature folder:
features/
leads/
components/
LeadCard.tsx
LeadForm.tsx
LeadFilter.tsx
hooks/
useLeadFilters.ts
useLeadMutation.ts
actions/
fetchLeads.ts
createLead.ts
updateLead.ts
types/
index.ts
utils/
formatLeadData.ts
validateLeadEmail.ts
index.ts (barrel export)
The index.ts barrel export is critical—it lets other parts of your app import cleanly:
// Instead of this:
import { LeadCard } from '../../../features/leads/components/LeadCard';
import { useLeadFilters } from '../../../features/leads/hooks/useLeadFilters';
// Do this:
import { LeadCard, useLeadFilters } from '@/features/leads';
I've used this structure on a CRM platform with 15+ features (leads, accounts, contacts, opportunities, tasks). Each team owned their feature folder. Merges were clean because people rarely touched each other's directories. When the leads team refactored their API integration, it didn't ripple through the codebase because the contract was well-defined in the barrel export.
One rule I enforce: Never import directly from another feature's internal folders. If you need something from another feature, it should be exported from their index.ts. This keeps coupling loose and makes it easy to move features around later.
Route Groups — Layouts Without URL Chaos
Route groups are underutilized in large Next.js apps. They let you organize routes with shared layouts without creating URL segments. In a typical app, you have at least two distinct visual shells: marketing pages (landing, pricing, blog) and authenticated dashboard pages (settings, reports, users). Without route groups, you'd either duplicate layouts or create awkward URL structures.
Route groups solve this by using parentheses:
app/
(marketing)/
layout.tsx
page.tsx (/)
pricing/
page.tsx (/pricing)
blog/
page.tsx (/blog)
(dashboard)/
layout.tsx
page.tsx (/dashboard)
leads/
page.tsx (/dashboard/leads)
[id]/
page.tsx (/dashboard/leads/[id])
settings/
page.tsx (/dashboard/settings)
The URLs are clean—/pricing and /blog, not /marketing/pricing. But the layouts are completely separate. The marketing layout might have a simple header and footer; the dashboard layout has a sidebar, top nav, and user menu.
On a travel SaaS I built, we had three route groups: (marketing), (app), and (admin). Each had its own layout hierarchy. A middleware checked authentication and redirected accordingly. If an unauthenticated user tried to access /dashboard, they got redirected to /login. No layout contamination, no URL mess.
Tip: Combine route groups with middleware for authentication-based routing. Check user role in middleware, then different route groups render entirely different interfaces for the same content.
Colocate Private Files — Keep Components Close to Where They're Used
A mistake I made early: dumping every component used within a route into a shared components/ folder. This works until you have 50 route-specific components scattered in a global folder and you're hunting for which ones are actually used and which are orphaned.
The solution: _components folders inside routes for components used only within that route. The underscore prefix tells Next.js these aren't route segments. They're just organizational helpers.
app/
(dashboard)/
leads/
_components/
LeadTableRow.tsx
LeadBulkActions.tsx
LeadStatusBadge.tsx
_hooks/
useLeadTableSort.ts
page.tsx
layout.tsx
contacts/
_components/
ContactCard.tsx
ContactForm.tsx
page.tsx
Components in _components are route-specific. If LeadTableRow.tsx is only used in the leads page, it lives there. This has massive benefits:
- No orphan hunting: If you delete the leads page, you know to clean up its
_componentsfolder. - Clear intent: Developers see that
LeadTableRowisn't shared; they don't accidentally import it from across the app. - Easier refactoring: You can rename, move, or restructure route-specific components without worrying about imports from unexpected places.
- Local state and logic: Route-specific hooks live alongside their components, making the relationship explicit.
On a ride-hailing app, I had driver and rider dashboards side by side. Each had 20+ route-specific components. Keeping them colocated in _components meant the two features evolved independently without accidentally sharing code they shouldn't.
Shared UI Separately — Your Design System Lives Here
components/ui is sacred. It contains only low-level, reusable design system components: buttons, inputs, modals, badges, cards, spinners. These are dumb, highly configurable, zero business logic components. They're used everywhere.
components/
ui/
Button.tsx
Input.tsx
Modal.tsx
Select.tsx
Badge.tsx
Card.tsx
Spinner.tsx
Table.tsx
Tooltip.tsx
layout/
Sidebar.tsx
Header.tsx
Footer.tsx
The Button component should never import from features/ or know about your business domain. It takes props—variant, size, onClick, loading—and renders. That's it. Similarly, components/layout contains layout primitives used across multiple route groups (like a reusable sidebar or header).
I've seen teams blur this line. A "Button" component grows logic to handle lead submission, validation, error states. Suddenly it's tied to the leads feature. Other features can't use it because it has leads-specific baggage. This breaks the abstraction.
The rule: If a component could be documented in Storybook and used in isolation without any business context, it belongs in components/ui. Everything else goes in features or route-specific _components.
Bringing It Together: A Real-World Example
Imagine a SaaS with accounts, users, and billing. Here's how I'd structure it:
app/
(marketing)/
layout.tsx
page.tsx
pricing/
page.tsx
(dashboard)/
layout.tsx
page.tsx
accounts/
_components/
AccountForm.tsx
AccountDeleteDialog.tsx
page.tsx
[id]/
page.tsx
users/
_components/
UserTable.tsx
UserInviteForm.tsx
page.tsx
billing/
_components/
PlanCard.tsx
InvoiceList.tsx
page.tsx
components/
ui/
Button.tsx
Dialog.tsx
Input.tsx
Table.tsx
layout/
DashboardHeader.tsx
DashboardSidebar.tsx
features/
accounts/
components/
AccountCard.tsx
hooks/
useAccount.ts
actions/
fetchAccounts.ts
types/
index.ts
index.ts
users/
components/
UserCard.tsx
hooks/
useUsers.ts
actions/
inviteUser.ts
types/
index.ts
index.ts
billing/
components/
(empty or shared across routes)
hooks/
useBillingPlans.ts
actions/
fetchPlans.ts
types/
index.ts
index.ts
Notice: features/ holds domain logic, types, hooks, and actions. Routes in app/ compose these with route-specific components from _components and shared UI from components/ui. Each developer knows exactly where to look for any piece of code.
Common Mistakes
Mistake 1: Feature folders with too much nesting. I've seen features/leads/components/forms/complex/LeadForm.tsx. Two levels deep is usually enough. If you need deeper organization, you might have too much in one feature.
Mistake 2: Putting business logic in components/ui. A component that needs to know about your app's state or features doesn't belong in UI. Move it to features or route-specific _components.
Mistake 3: Ignoring the barrel export. If developers start importing from features/leads/hooks/useLeadFilters directly, you've lost the abstraction. Make the barrel export a house rule.
Mistake 4: Using route groups for organization instead of layout separation. Route groups aren't just folder tidiness; they exist to separate layouts. If you don't need different layouts, don't use them.
Mistake 5: Global utils/ folder. A massive utils/formatDate.ts, utils/validateEmail.ts, utils/parseJSON.ts folder becomes a dumping ground. Utilities should live where they're used: in features, in route-specific folders, or in a tightly organized lib/ folder for truly universal helpers (maybe 3-5 files).
Key Takeaways
- Feature folders group domain-related components, hooks, actions, and types together, making the codebase predictable and preventing duplication.
- Route groups in
app/separate distinct visual shells (marketing, dashboard, admin) without polluting URLs or duplicating layouts. - Colocate with underscores: Use
_componentsand_hooksfolders inside routes for code used only within that route. - Design system discipline: Keep
components/uifocused on dumb, reusable UI primitives with zero business logic. - Barrel exports matter: Make feature exports the single source of truth. Enforce the rule: import from
@/features/leads, not@/features/leads/hooks. - This structure survives 200+ components because each file has a clear purpose and a predictable location. New developers can find what they need without asking.
I've applied this structure to platforms handling thousands of concurrent users, complex state management, and rapid feature development. It's not perfect for every team or project, but it scales well and survives real-world chaos better than most alternatives I've tried.






Comments
Loading comments…