WritingNext.js Project Structure Best Practices for Scalable Applications — Clixo
6 min readnext.js, project-structure, architecture, best-practices, app-router

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 with import 'server-only'.
  • Hooks in src/hooks/ are always client-side. They can be imported from use client files 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.