Next.js Project Structure Best Practices for Scalable Applications
How to structure a Next.js App Router project for long-term maintainability — folder conventions, colocation, shared code patterns, and what to avoid as teams grow.
A growing Next.js project needs clear places for routes, feature components, shared utilities, and server code. Explicit boundaries help developers locate code and understand who depends on it.
Use the following patterns as starting points, then adapt them to the application’s feature boundaries.
The Core Principle: Colocation
The App Router actively encourages colocation — placing components, hooks, and utilities close to the routes that use them. A component used only by one route should live next to that route, not in a global components folder.
Only things that are genuinely shared across multiple unrelated routes belong in a shared folder.
A flat shared folder makes it hard to tell whether a component belongs to one feature or has consumers across the application. Keep that ownership visible in the directory structure.
A Working Folder Structure
app/
(marketing)/
page.tsx
about/
page.tsx
layout.tsx
_components/
hero.tsx ← only used in marketing routes
testimonials.tsx
(dashboard)/
layout.tsx
settings/
page.tsx
_components/
settings-form.tsx ← only used in settings
analytics/
page.tsx
src/
components/
ui/ ← shared primitive components (Button, Input, Modal)
layout/ ← shared layout components (Header, Footer, Sidebar)
lib/
db.ts ← database client
auth.ts ← auth utilities
validations.ts ← shared Zod schemas
hooks/ ← shared custom hooks (use client)
types/ ← shared TypeScript types
constants/ ← application-wide constants
The _components folders (prefixed with an underscore) are a convention in the App Router for private folders — Next.js excludes them from routing, so they can live inside the app directory without creating routes.
Route-Level Colocation in Practice
When a route has its own components, data-fetching functions, and types, keeping them together makes the route self-contained.
app/(dashboard)/orders/
page.tsx
_components/
orders-table.tsx
order-status-badge.tsx
_lib/
get-orders.ts ← server-only data fetching
_types/
order.ts
Anyone working on the orders section can find everything they need without leaving the folder. Deleting the orders route means deleting one folder.
The src/components/ui Layer
Primitive components — buttons, inputs, modals, dropdowns — should be abstracted into a shared UI layer that knows nothing about your application's business domain. This layer is where design system components live.
These components should:
- Accept only generic, UI-focused props (no business objects)
- Be client components if they need interactivity
- Have no knowledge of routing, data fetching, or application state
- Be documented and stable enough to be used anywhere
The shadcn/ui library is a popular starting point for this layer. It generates source files you own rather than a dependency you import — which means you can modify components without fighting a library's API.
Separating Server and Client Code
In a project with many server components and client components, import mistakes are easy. A utility that imports server-only will break if accidentally pulled into a client component.
Enforce the boundary with conventions:
- Functions and modules in
src/lib/are server-only by default. Mark them withimport 'server-only'. - Hooks in
src/hooks/are always client-side. They can be imported fromuse clientfiles safely. - Pure utilities in
src/utils/(formatters, string helpers, math) should have no side effects and no server or browser dependencies — safe on either side.
Import Paths
Long relative imports (../../../../../../components/button) are a maintenance burden and a sign that the file is in the wrong place.
Configure path aliases in tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@ui/*": ["./src/components/ui/*"]
}
}
}With this configuration, any file can import from @/lib/db or @ui/button regardless of its location. Reorganizing files does not require updating import paths across the codebase.
Server Actions Organization
Server Actions are async functions marked with 'use server'. As an application grows, they accumulate. Two approaches work well:
Colocated actions: Place actions in _actions/ folders next to the routes that use them. An order cancellation action lives with the orders route.
Centralized actions: Place all actions in src/actions/, organized by domain (src/actions/orders.ts, src/actions/auth.ts). This works better for actions called from multiple routes.
Mixing both approaches within the same project is confusing. Choose one and apply it consistently.
What to Avoid
Global components folder with everything. Without a colocation discipline, every component ends up here. After a year, it becomes impossible to know what is used where.
Deeply nested folder hierarchies. More than three levels of nesting for non-route folders is usually a sign that the domain model needs re-examining, not more folders.
Business logic in page files. Page files should compose components and pass data. Business logic — rules about what a user can do, how data should be transformed — belongs in the lib layer.
No separation between server-only and shared utilities. This leads to runtime errors in edge cases that are painful to debug.
Grow the Structure Intentionally
Start lean. A small project does not need all these folders on day one. Start with app/, src/components/ui/, and src/lib/. Add _components colocation when a route accumulates more than two or three local components. Add the domain structure when you have more than one major feature area.
Structure should reflect the actual complexity of the application, not anticipate complexity that has not arrived.
Document the structure and apply it consistently when adding features. Revisit a boundary when imports or ownership become difficult to explain.