Next.js

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated

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:

bash
/app
  /dashboard
    page.tsx
/components
  Button.tsx
  DashboardSidebar.tsx
  UserProfileCard.tsx
  DatePicker.tsx
/lib
  supabase.ts
  utils.ts

Why 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:

bash
/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.ts

1. 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:

tsx
// 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.

Tip

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.

typescript
// 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#

  1. Delete-ability: If you decide to remove a feature (e.g., a "Referral" system), you just delete the /features/referrals folder, rather than hunting for scattered hooks, components, and API routes across ten global folders.
  2. Onboarding: New developers know exactly where to look. Need to fix a bug in billing? You don't scour the global components folder; you go straight to features/billing.
  3. 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.

More on This Topic#

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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

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.