How to Structure Your Next.js App Router Project for Scale
Stop shoving everything into the components folder. Learn the Feature-Sliced Design pattern adapted perfectly for the Next.js App Router.
Introduction#
Next.js App Router gives you unparalleled freedom. Aside from the mandatory routing conventions (page.tsx, layout.tsx), Next.js doesn't enforce how you should organize your UI components, hooks, utilities, or database calls.
For small projects, a simple components and lib folder works perfectly. But at scale—when your repository hits dozens of routes and millions of lines of code—the "flat folder" approach collapses into a tangled web of circular dependencies and unmaintainable code.
In 2026, the enterprise standard for Next.js organization is an adaptation of Feature-Sliced Design (FSD) natively integrated with App Router capabilities.
The "Everything in Components" Problem#
The most common Next.js beginner folder structure looks like this:
/app
/dashboard
page.tsx
/components
Button.tsx
DashboardSidebar.tsx
UserProfileCard.tsx
DatePicker.tsx
/lib
supabase.ts
utils.tsWhy does this fail at scale? Because Button.tsx (a generic UI component) lives right next to DashboardSidebar.tsx (a highly specific, domain-coupled component that likely fetches data). When a new developer joins the team, they have no idea which components are universally safe to reuse and which are deeply tied to specific pages.
The Feature-Centric Approach#
Instead of grouping files by their type (e.g., "all hooks go in a /hooks folder"), group files by their feature or domain.
Here is the ideal scalable structure:
/src
/app # only routing logic
/dashboard
page.tsx
layout.tsx
/components # purely generic, reusable UI (Design System)
/ui
Button.tsx
Input.tsx
Dialog.tsx
/features # Domain-specific modules
/auth # "Auth" feature bundle
/components
LoginForm.tsx
/actions
login.ts
/hooks
useAuth.ts
/billing # "Billing" feature bundle
/components
PricingTable.tsx
/actions
checkout.ts
/queries
get-subscription.ts
/lib # truly global utilities
supabase.ts
utils.ts1. The app Directory (Routing Only)#
Your app directory should be surprisingly thin. It should act purely as the router and entry point.
A typical page.tsx should just parse search params, run authorization checks, and render a high-level feature component:
// app/dashboard/billing/page.tsx
import { BillingDashboard } from '@/features/billing/components/BillingDashboard'
import { getSubscriptionStatus } from '@/features/billing/queries/get-subscription'
export default async function Page() {
const status = await getSubscriptionStatus()
return <BillingDashboard initialStatus={status} />
}2. The components/ui Directory (Design System)#
This directory should only contain dumb, stateless UI components. These components should never fetch their own data or know anything about your business logic.
If you use shadcn/ui, this is where those components live securely decoupled from your domain.
3. The features Directory (The Core)#
This is where the magic happens. A "Feature" is a self-contained slice of domain logic. By isolating auth, billing, and projects into their own directories, you enforce strict architectural boundaries.
A file inside /features/billing is absolutely forbidden from importing from /features/auth/internal.
The index.ts rule: Every feature folder should have an index.ts file that exports only what other features are allowed to use. This creates a clear public API for your modules.
// src/features/billing/index.ts
export { PricingTable } from './components/PricingTable'
export { getSubscriptionStatus } from './queries/get-subscription'
export type { SubscriptionPlan } from './types'4. Server Actions vs Data Queries#
Notice how inside a feature folder we split /actions and /queries:
- /queries: Read operations. These are standard async functions that fetch data (usually wrapped in Next.js
cache). - /actions: Write operations. These files have the
'use server'directive at the top and handle mutations from the client.
By keeping these separate, you prevent accidentally exposing sensitive database query logic to the client side.
Why This Architecture Wins#
- Delete-ability: If you decide to remove a feature (e.g., a "Referral" system), you just delete the
/features/referralsfolder, rather than hunting for scattered hooks, components, and API routes across ten global folders. - Onboarding: New developers know exactly where to look. Need to fix a bug in billing? You don't scour the global
componentsfolder; you go straight tofeatures/billing. - Colocation: Tests live right next to the code they test. Server actions live right next to the components that call them.
Adopting a feature-centric structure early feels like overkill, but by the time your Next.js app hits production, it will be the only thing keeping your codebase sane.
- Next.js + Supabase App Router hub
- Next.js Server Actions with Supabase: Complete Production Guide
- Next.js Server Actions vs API Routes: When to Use Each
- Fix "cookies() should be awaited" Error in Next.js 15 App Router (Complete Migration Fix 2026)
- React Server Components: Complete Deep Dive
The cost of the index.ts barrel-export convention#
The index.ts public-API pattern this page recommends for each feature folder — a
single file re-exporting everything a feature makes available to the rest of the app —
is good for enforcing boundaries, but it isn't free at build time, and the tradeoff is
worth naming honestly. A barrel file that re-exports dozens of components forces the
bundler to resolve every one of those exports even when a consuming file only imports
one or two of them, because it can't always tell in advance which named exports are
actually used downstream. On a large feature with many components sharing one
index.ts, this shows up as slower cold builds and, in dev mode, slower Fast Refresh —
exactly the kind of build-time regression documented for MUI's icon and component
barrels in Fix barrel_optimize Build Warnings with MUI in Next.js.
The mitigation isn't to abandon the pattern — the architectural clarity it buys is real
— but to keep each feature's barrel narrow: export only what genuinely needs to cross
the feature boundary (a handful of components, one or two server actions), not every
internal helper. A feature with 20+ internal files should still expose a 3-5-line
index.ts, not one that mirrors the whole folder. If a specific barrel does become a
build bottleneck, Next.js's experimental.optimizePackageImports config (already the
default for several popular libraries, including @mui/material) can be pointed at first-party barrels too, so
the bundler resolves only the named imports actually used rather than the whole
re-export graph.
Further reading: Offline-First Next.js + Supabase: Sync That Actually Works.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Offline-First Next.js + Supabase: Sync That Actually Works
A typed Dexie queue, stable sync lifecycle and account-scoped cache, with explicit boundaries for offline edits and mocked failure-path tests.
Supabase + Next.js Tutorial for Beginners (Free, 2026)
Build a complete full-stack application with Next.js and Supabase from scratch. Authentication, database, CRUD operations, and deployment — all in 20 minutes.
7 Next.js + Supabase Architecture Decisions I'd Skip
After shipping multiple production apps with Next.js and Supabase, here are the decisions that cost the most time to undo — and what I'd do instead from day one.


