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
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:
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.
// 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:
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.
// 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)
// 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/:
// 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
// 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:
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.
Continue reading
Related articles
Rate Limiting in Next.js: Protecting Your API Routes
How to implement production-grade rate limiting in Next.js — with Middleware-level protection, per-user limits, and distributed rate limiting using Upstash Redis.
EngineeringNext.js Parallel Routes and Intercepting Routes: A Complete Guide
Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.
EngineeringVercel vs Netlify vs AWS Amplify for Next.js in 2026
A practical comparison of the three most common Next.js hosting platforms — Vercel, Netlify, and AWS Amplify — with real cost and capability trade-offs.
Stay informed
Get our monthly deep dives.
Engineering, design, and growth insights — once a month. No spam.
Browse all resources