# 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.

- **Published:** 2026-01-19
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** next.js, project-structure, architecture, best-practices, app-router
- **Canonical URL:** https://clixo.sh/blog/nextjs-project-structure-best-practices

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.

```mermaid
flowchart TD
  ROOT["Project root"] --> APP["app/ — routes and layouts"]
  ROOT --> SRC["src/ — shared code"]
  APP --> MKT["(marketing)/ — route group"]
  APP --> DASH["(dashboard)/ — route group"]
  MKT --> MC["_components/ — route-local components"]
  DASH --> DC["_components/ + _lib/ + _types/"]
  SRC --> UI["components/ui — primitive components"]
  SRC --> LIB["lib/ — server-only utilities"]
  SRC --> HOOKS["hooks/ — client hooks"]
  SRC --> TYPES["types/ + constants/"]
```

## 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`:

```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](https://clixo.sh/#contact) for product teams.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
