How to Structure Your Next.js App Router Project for Scale
technology

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.

2026-04-02
How to Structure Your Next.js App Router Project for Scale

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

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

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

  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.