All insights
Engineering8 min read

How to Structure a Next.js Project for Scale

A battle-tested folder structure for Next.js App Router projects — from small MVPs to large SaaS applications with multiple teams.

The Problem with Flat Structure

Most Next.js tutorials start with everything in app/ and components/. That works fine for a demo. It falls apart when you have 30 developers, 200 components, and 50 API routes — because nobody agrees on where anything lives.

Good project structure makes the right thing easy to find and the wrong thing hard to do accidentally.

The Recommended Structure

code
src/
  app/                    ← Next.js App Router routes
    (marketing)/          ← route group — no URL prefix
      page.tsx            ← /
      about/
        page.tsx          ← /about
    (app)/                ← authenticated app routes
      layout.tsx          ← shared auth layout
      dashboard/
        page.tsx          ← /dashboard
      settings/
        page.tsx          ← /settings
    api/                  ← Route Handlers
      auth/
        [...nextauth]/
          route.ts
      webhooks/
        stripe/
          route.ts
    layout.tsx            ← root layout
    globals.css

  components/
    ui/                   ← design system primitives (Button, Input, Modal)
    layout/               ← structural components (Navbar, Footer, Sidebar)
    sections/             ← page-level sections (HeroSection, PricingTable)
    forms/                ← form components with validation
    [feature]/            ← feature-specific components

  lib/
    auth.ts               ← auth utilities
    db.ts                 ← database client
    validations.ts        ← Zod schemas
    utils.ts              ← generic helpers

  hooks/                  ← custom React hooks
  types/                  ← TypeScript types and interfaces
  data/                   ← static data and constants
  config/                 ← app configuration

Route Groups

Route groups (folders wrapped in parentheses) let you organise routes without affecting the URL structure:

code
app/
  (marketing)/
    page.tsx        → /
    pricing/
      page.tsx      → /pricing
  (app)/
    layout.tsx      ← requires auth
    dashboard/
      page.tsx      → /dashboard

Both the marketing pages and app pages live under the same app/ directory, but can have completely different layouts and middleware behaviour.

Components: Three Layers

Keep three distinct layers:

components/ui/ — primitives that have no business logic. Button, Input, Badge, Card, Modal. These are your design system. They accept props, they render HTML, they know nothing about your app.

code
// components/ui/Button.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
}

export function Button({ variant = 'primary', size = 'md', ...props }: ButtonProps) {
  return <button className={buttonVariants({ variant, size })} {...props} />
}

components/sections/ — composed page sections. HeroSection, PricingTable, TestimonialSection. These understand your domain but not specific pages.

Feature components live inside or next to their feature:

code
app/(app)/dashboard/
  page.tsx
  _components/          ← private components, only used here
    MetricsGrid.tsx
    ActivityFeed.tsx

The underscore prefix _components is a Next.js convention to exclude a folder from routing.

The lib/ Directory

lib/ is for code that's shared across routes but isn't a component. Keep it pure — no React, no JSX.

code
// lib/db.ts — database client singleton
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'

const client = postgres(process.env.DATABASE_URL!)
export const db = drizzle(client)
code
// lib/auth.ts — auth helpers
export async function getCurrentUser(request?: Request) { ... }
export async function requireAuth(request: Request) { ... }

Avoid lib/utils.ts becoming a junk drawer. If a utility grows beyond 5–10 functions, split it: lib/date.ts, lib/strings.ts, lib/currency.ts.

Data Fetching: Where Does It Live?

Simple data access: directly in Server Components using your lib/db.ts.

Reusable queries: extract to lib/queries/ or lib/data/:

code
// lib/queries/projects.ts
export async function getProjectsByUser(userId: string) {
  return db.query.projects.findMany({
    where: eq(projects.userId, userId),
    orderBy: desc(projects.createdAt),
  })
}

Never put database queries in components. Components render — data fetching is a separate concern.

TypeScript Conventions

code
// types/index.ts — re-export for convenience
export type { User } from './user'
export type { Project } from './project'

// types/user.ts
export interface User {
  id: string
  email: string
  name: string
  plan: 'free' | 'pro' | 'enterprise'
}

Avoid mixing runtime types (Zod schemas) and TypeScript types in the same file — they serve different purposes.

Scaling the Structure

For large teams, consider module-based organisation:

code
src/
  modules/
    auth/
      components/
      hooks/
      lib/
      types/
    billing/
      components/
      hooks/
      lib/
      types/
    projects/
      ...

This co-locates everything related to a feature. A new developer can understand the billing module without reading the entire codebase. Teams can own modules independently.

What to Avoid

Deep nesting. More than 4 levels of folder depth is a smell. Flatten where possible.

index.ts barrel files everywhere. They look clean but kill tree-shaking and make "go to definition" jumps circular. Use them sparingly, only for public API surfaces.

Mixing server and client code in the same file without clear marking. Use 'use client' and 'use server' directives deliberately. If a file gets confusing, split it.

The best structure is the one your whole team can navigate without asking questions. Start simple, refactor as the codebase grows — don't over-engineer for scale you haven't reached yet.

Stay informed

Get our monthly deep dives.

Engineering, design, and growth insights — once a month. No spam.

Browse all resources