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 poorly structured Next.js project starts causing friction around the time you add a third developer or a second major feature area. Components pile up in a flat components folder, imports grow long and unpredictable, and nobody is sure where new code should go. Fixing the structure later means touching dozens of files and wrestling with import paths across the codebase.
Getting the structure right early — or refactoring it intentionally — pays off consistently. This guide outlines the patterns that hold up as Next.js projects grow.
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.
This sounds obvious, but most teams ignore it and default to a flat shared structure because it feels safer. The result is a components folder with 80 files and no way to know which ones can be safely deleted.
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.
A well-structured project is not a constraint — it is what lets a team move fast without accumulating confusion. The patterns here are durable enough to carry a project from prototype to production and beyond.
If you're starting a Next.js project and want an engineering team that builds with this level of care from the beginning, Clixo designs and builds custom software for product teams.