How to Structure Your Next.js Project for Scale
A practical guide to organizing your Next.js codebase — directory structures, feature-based architecture, shared components, and configuration patterns that scale.
Every Next.js project starts clean. Then features pile up, the components folder turns into a graveyard of single-use files, and nobody can find anything. If you're brand new to the framework, our Getting Started with Next.js guide is a good first stop. I've refactored enough codebases to know that structure problems don't fix themselves — they compound.
Here's how I organize Next.js projects that stay navigable as they grow.
The Default Structure Breaks Down Fast
The out-of-the-box Next.js structure gives you app/, public/, and not much else. For a landing page, that's fine. For anything with multiple features, you need a plan.
The two main approaches are layer-based and feature-based organization. Most teams should use a hybrid.
Layer-Based Structure
Group files by their role in the application:
src/
app/ # Routes and pages
components/ # Shared UI components
lib/ # Utilities, helpers, config
hooks/ # Custom React hooks
types/ # TypeScript type definitions
styles/ # Global styles
constants/ # App-wide constants
This works well for small-to-medium projects. The downside: when you have 50 components in components/ and 30 functions in lib/, finding what belongs to what becomes painful.
Feature-Based Structure
Group files by what they do, not what they are:
src/
app/
features/
auth/
components/
hooks/
actions/
types.ts
dashboard/
components/
hooks/
actions/
types.ts
billing/
components/
hooks/
actions/
types.ts
components/ # Truly shared components
lib/ # Truly shared utilities
Each feature is self-contained. When you work on billing, everything you need is in features/billing/. When you delete a feature, you delete one folder.
The Hybrid Approach I Use
For most Next.js projects, I use a combination:
src/
app/
(marketing)/
page.js
about/
page.js
(app)/
dashboard/
page.js
layout.js
settings/
page.js
api/
webhooks/
route.js
layout.js
components/
ui/ # Base UI primitives (Button, Input, Card)
layout/ # Header, Footer, Sidebar, Navigation
shared/ # Reusable composed components
features/
auth/
billing/
analytics/
lib/
db.ts
utils.ts
validations.ts
hooks/
config/
site.ts
navigation.ts
Route Groups
The (marketing) and (app) parenthesized folders are route groups — they organize routes without affecting the URL. Your marketing pages get one layout, your app pages get another, and the URL stays clean. Route groups are just one of several App Router conventions worth knowing — see our full Next.js App Router guide.
The components/ui Convention
Base UI components — buttons, inputs, cards, modals — live in components/ui/. These are generic building blocks with no business logic. If you're using a component library like Spell UI, installed components naturally land in this directory and follow the same pattern.
Feature-specific components live inside their feature folder. The rule: if a component is used by more than one feature, it moves to components/shared/.
Configuration Patterns
Centralize configuration instead of scattering magic values:
// config/site.ts
export const siteConfig = {
name: "My App",
description: "A description of my app",
url: "https://myapp.com",
links: {
github: "https://github.com/myapp",
docs: "https://docs.myapp.com",
},
}
// config/navigation.ts
export const mainNav = [
{ title: "Features", href: "/features" },
{ title: "Pricing", href: "/pricing" },
{ title: "Docs", href: "/docs" },
]
export const dashboardNav = [
{ title: "Overview", href: "/dashboard" },
{ title: "Analytics", href: "/dashboard/analytics" },
{ title: "Settings", href: "/dashboard/settings" },
]
This makes it trivial to update navigation, reuse metadata, and keep things consistent.
Server vs. Client Component Boundaries
Structure reinforces the server/client split. I keep a clear convention:
- Server components are the default. Pages, layouts, and data-fetching components stay on the server.
- Client components get a
"use client"directive and are typically interactive leaf nodes — form inputs, modals, dropdown menus. - Barrel exports (
index.tsfiles) can cause accidental client bundling. Be explicit about imports.
Pushing client boundaries to the leaves is also one of the highest-impact patterns in our 10 Next.js performance tips — keeping components on the server ships less JavaScript.
A common pattern is wrapping a client component inside a server component that fetches its data:
// features/dashboard/components/stats-section.js (server)
import { getStats } from "../actions"
import { StatsChart } from "./stats-chart" // client component
export async function StatsSection() {
const stats = await getStats()
return <StatsChart data={stats} />
}
Naming Conventions
Consistency matters more than which convention you pick. Here's what I use:
- Files: kebab-case (
user-profile.tsx,use-auth.ts) - Components: PascalCase (
UserProfile,StatsChart) - Utilities: camelCase (
formatDate,calculateTotal) - Constants: SCREAMING_SNAKE_CASE (
MAX_RETRY_COUNT)
The Real Rule
The best project structure is the one your team actually follows. Pick a pattern, document it in a CONTRIBUTING.md, and enforce it with linting rules. A mediocre structure followed consistently beats a perfect structure followed loosely. The same principle applies to feature subsystems — see our guides on Next.js API routes and Server Actions and authentication in Next.js for opinionated layouts you can reuse.
Start simple. Refactor when the pain is real, not when it's theoretical. Your future self will thank you for the discipline.