Type something to search...
Next.js Project structure and organization

Next.js Project structure and organization

Every Next.js project starts the same way: a fresh app directory with a page.tsx and a layout.tsx, and nothing else. It looks simple, almost too simple, and that simplicity is exactly what gets teams into trouble six months later. Because Next.js is deliberately unopinionated about where you put your components, your hooks, or your utility functions, a project with no agreed-upon structure tends to sprawl — some files live next to the routes that use them, others get dumped in a catch-all components folder, and eventually nobody can predict where a new file should go without asking in Slack.

The good news is that Next.js gives you more structural tools than most developers ever use. Route groups, private folders, parallel routes, colocation — these aren't just routing mechanics, they're organizational primitives. Once you understand what each one actually does, you can build a folder structure that scales from a five-page marketing site to a multi-team dashboard application without a rewrite. This article walks through every file and folder convention the App Router recognizes, then gets into the part the documentation only gestures at: which structure to actually pick, and why.

The Big Idea: Folders Route, Files Render

Before getting into specifics, it helps to internalize one rule that explains almost everything else in this article: in the app directory, folders define URL structure, and files define what gets rendered.

A folder by itself does nothing. app/blog/authors/ creates no route on its own. It's only when you drop a page.tsx (or a route.ts) inside that folder that Next.js exposes it as a real, navigable, publicly accessible URL. This single fact is the reason colocation works at all — you can nest as many folders as you want for organizational purposes, and none of them become routes unless you explicitly add a page or route file. Keep this rule in mind as you read the rest of this article; almost every organizational pattern below is really just a consequence of it.

Top-Level Folders

At the root of a Next.js project, four folders have special meaning:

FolderPurpose
appThe App Router — where your routes, layouts, and route handlers live
pagesThe legacy Pages Router, if you're using it alongside or instead of app
publicStatic assets served as-is at the root URL (images, fonts, favicons, robots.txt)
srcAn optional wrapper folder that holds app (and everything else) to separate application code from root-level config

Most new projects only need app and public. The src folder is worth a special mention because it's one of those decisions that's easy to make on day one and painful to reverse later — more on that below.

Top-Level Files

Outside of app, your project root accumulates a predictable set of configuration files:

  • next.config.js — the single file that controls almost every build-time and runtime behavior: redirects, rewrites, image domains, experimental flags, and more.
  • package.json — standard npm/yarn/pnpm dependency and script manifest.
  • instrumentation.ts — a hook for wiring up OpenTelemetry or custom monitoring at server startup.
  • proxy.ts — the App Router's request proxy, which runs before a request is routed (this is the file that used to be called middleware.ts in older Next.js versions — if you're coming from training data or an older tutorial that mentions middleware.ts, know that current Next.js has renamed this convention to proxy.ts).
  • .env, .env.local, .env.development, .env.production — environment variable files, none of which should be committed to version control except as .env.example templates.
  • eslint.config.mjs — ESLint configuration, using the newer flat-config format.
  • tsconfig.json / jsconfig.json — TypeScript or JavaScript project configuration, including path aliases like @/*.
  • next-env.d.ts — an auto-generated TypeScript declaration file. Never edit this by hand, and don't remove it from .gitignore.

None of these files are things you'll touch daily, but knowing what each one is responsible for saves you from the classic mistake of, say, trying to configure image domains in tsconfig.json because you can't remember which file owns which concern.

Routing Files: The Vocabulary of the app Directory

Inside app, Next.js recognizes a specific set of special file names. Each one plays a distinct role, and understanding what each does — not just that it exists — is what lets you reason about a route segment at a glance.

app/
├── layout.tsx        # Shared UI wrapper — persists across navigation
├── page.tsx          # Makes the segment a public, navigable route
├── loading.tsx        # Suspense fallback UI shown while the segment loads
├── error.tsx          # Error boundary for this segment and its children
├── not-found.tsx      # UI shown when notFound() is called or a route doesn't match
├── template.tsx       # Like layout, but remounts on every navigation
├── route.ts          # Defines an API endpoint instead of a page
└── default.tsx        # Fallback UI for unmatched parallel route slots

A few of these deserve more than a one-line description because their behavior is easy to get wrong in practice.

layout.tsx vs template.tsx is the pair people mix up most often. A layout preserves component state across navigations within the same segment — if you have a layout with a form or a scroll position, navigating between child pages won't reset it. A template, by contrast, creates a brand-new instance of its component tree on every navigation, which means any local state resets. You almost always want layout. Reach for template only when you specifically need that remount behavior — for example, to re-trigger a CSS animation on every page transition, or to reset a useState value that shouldn't persist between child routes.

error.tsx is a Client Component boundary, full stop. It must include "use client" at the top, because React error boundaries only work on the client. This trips people up because every other special file in this list can be a Server Component by default.

// app/dashboard/error.tsx
"use client";

import { useEffect } from "react";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log the error to a reporting service here
    console.error(error);
  }, [error]);

  return (
    <div className="p-6 text-center">
      <h2 className="text-lg font-semibold">Something went wrong</h2>
      <button
        onClick={reset}
        className="mt-4 rounded bg-black px-4 py-2 text-white"
      >
        Try again
      </button>
    </div>
  );
}

The reset function it receives doesn't reload the page — it attempts to re-render the segment, which is useful for transient errors like a failed fetch that might succeed on retry.

route.ts and page.tsx cannot coexist in the same segment. A folder is either a page or an API endpoint at a given path — you can't have app/api/users/page.tsx and app/api/users/route.ts side by side. This is a natural consequence of the "folders route, files render" rule: the folder just marks the URL segment, and only one rendering file gets to claim it.

Nested Routes and the Component Hierarchy

Folders nest, and so do the URL segments they represent. app/blog/authors/page.tsx becomes /blog/authors automatically, no configuration required. What's less obvious is how the special files at each level compose together when a route actually renders.

When you navigate to a route, Next.js renders the special files in this fixed order, nesting each one inside the one before it:

layout.tsx
  → template.tsx
    → error.tsx (boundary)
      → loading.tsx (suspense boundary)
        → not-found.tsx (boundary)
          → page.tsx (or nested layout.tsx)

This ordering matters in practice. Because error.tsx wraps loading.tsx, an error boundary defined at a given segment can catch errors thrown while that segment's own loading state is active. And because layouts nest outward-in for parent segments and inward-out for children, a root layout's <html> and <body> tags always end up wrapping everything else, no matter how deep the matched route is.

Dynamic Routes

Square-bracket syntax parameterizes a segment:

ConventionExample folderMatches
[segment]app/blog/[slug]/page.tsx/blog/hello-world
[...segment]app/shop/[...slug]/page.tsx/shop/clothing, /shop/clothing/shirts
[[...segment]]app/docs/[[...slug]]/page.tsx/docs, /docs/getting-started, and deeper

The distinction between the last two is the one worth committing to memory: a catch-all ([...slug]) requires at least one path segment after the parent — /shop alone would 404 — while an optional catch-all ([[...slug]]) also matches the parent path with nothing after it. This is the pattern you want for something like a CMS-driven docs site where /docs itself should render a valid page, not 404.

Inside the page component, you read the matched segments through the params prop:

// app/blog/[slug]/page.tsx
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <h1>Post: {slug}</h1>;
}

Note that params is a Promise — this is one of the App Router details that's easy to get wrong if you're working from an older tutorial, since earlier versions passed params as a plain object. In current Next.js, both params and searchParams are asynchronous, and you need to await them before use.

Route Groups and Private Folders: Organization Without URL Impact

This is where Next.js gives you real organizational leverage, and where most of the "which folder structure should I use" debate actually lives.

Private folders — any folder prefixed with an underscore, like _components or _lib — are completely invisible to the router. Next.js won't ever treat anything inside a _folder as a route, no matter how deeply nested. This is different from plain colocation (which already works without any prefix, since only page and route files are ever routable) — the underscore convention exists purely as a signal, both to your routing system and to your teammates, that "this folder is implementation detail, not a route waiting to happen."

app/
└── dashboard/
    ├── page.tsx
    ├── _components/
    │   └── RevenueChart.tsx
    └── _lib/
        └── formatCurrency.ts

The practical reason to reach for this over plain colocation: it avoids naming collisions with future Next.js conventions. If Next.js ever ships a new special file called, say, chart.tsx, a plain components/chart.tsx folder could silently start behaving differently. A _components/chart.tsx folder never will, because the underscore opts the entire subtree out of the routing system.

Route groups — a folder wrapped in parentheses, like (marketing) — do the opposite job. They let you nest folders for organizational reasons without those folder names appearing in the URL:

app/
├── (marketing)/
│   ├── layout.tsx      # Layout only for marketing pages
│   ├── page.tsx        # renders at /
│   └── about/
│       └── page.tsx    # renders at /about
└── (shop)/
    ├── layout.tsx      # Different layout for shop pages
    └── cart/
        └── page.tsx    # renders at /cart

Both (marketing) and (shop) disappear from the URL entirely — /about and /cart, not /(marketing)/about and /(shop)/cart. What you get in exchange is the ability to give each group of routes its own layout, even though they'd otherwise share the same position in the route tree. This is the mechanism behind several patterns worth calling out individually:

Multiple root layouts. If you delete the top-level app/layout.tsx and instead give each route group its own layout.tsx — each including its own <html> and <body> tags — you get genuinely separate root layouts for different sections of the same app. This is the right call when, say, your marketing site and your logged-in dashboard need fundamentally different fonts, providers, or even different <html lang> values, and forcing them through one shared root layout would mean conditionally rendering half of it.

Opting a subset of routes into a layout. You don't have to group everything. If account and cart should share a layout but checkout shouldn't, put only account and cart inside a (shop) group and leave checkout as a sibling outside it. The URL doesn't change for any of the three; only the applied layout does.

Scoping a loading skeleton to one route. This one is subtle enough that most teams discover it by accident. If you add loading.tsx directly at app/dashboard/loading.tsx, it applies to every route under /dashboard, including deeply nested ones, because loading states inherit down the tree. If you only want a skeleton on /dashboard/overview specifically, wrap just that route in its own group — app/dashboard/(overview)/page.tsx and app/dashboard/(overview)/loading.tsx — so the loading boundary only applies within that group, not the whole /dashboard subtree.

A word of caution: route groups are easy to overuse. Every group you add is one more set of parentheses your team has to mentally parse when scanning the folder tree, and one more place layouts can silently diverge. Reach for a route group when you have a genuine reason — a distinct layout, a scoped loading state — not as a default organizational habit.

Parallel and Intercepted Routes

Two more conventions exist specifically for advanced UI patterns, and they're worth knowing about even if you don't reach for them often.

Parallel routes (@slot) let a single layout render more than one page simultaneously, each independently navigable. A dashboard with a persistent sidebar and a main panel that can each show different content based on the URL is the textbook use case — @sidebar and @main become props on the parent layout, each fed by its own subtree.

Intercepted routes use a family of dot-prefixed conventions — (.)folder, (..)folder, (..)(..)folder, and (...)folder — to render one route's UI inside the current layout while the browser's URL still updates to match the intercepted route. The canonical example is a photo grid where clicking a thumbnail opens a full detail view as a modal overlaying the grid, but refreshing that URL directly (or sharing the link) still loads the full, non-modal detail page. The number of dots controls how many segment levels up the interception reaches — same level, one level up the tree, two levels up, or all the way from the root.

Both of these are genuinely more complex than anything else in this article, and neither is something you should reach for on a first pass at a feature. Build the straightforward version first; revisit with parallel or intercepted routes only once a real UX requirement — a modal that needs its own shareable URL, a dashboard with independently-loading panels — demands it.

Metadata File Conventions

Next.js also recognizes a set of file-based conventions purely for metadata — favicons, Open Graph images, sitemaps, and robots directives — that live directly in app (or any route segment) rather than being manually linked in a <head> tag:

  • favicon.ico, icon.(ico|jpg|png|svg), apple-icon.(jpg|png) — app icons, either as static files or generated programmatically from icon.tsx.
  • opengraph-image.(jpg|png|gif) and twitter-image.(jpg|png|gif) — social preview images, static or generated.
  • sitemap.xml (or a generated sitemap.ts) and robots.txt (or robots.ts) — SEO files that Next.js will serve at the correct root-level path even if the source file lives elsewhere.

The advantage of the code-generated variants (icon.tsx, sitemap.ts) is that they can pull from your actual data — a sitemap.ts that queries your CMS for every blog slug, or an opengraph-image.tsx that renders a dynamic image per blog post using ImageResponse, rather than a single static image shared across every page.

Colocation: Why "Files Next To Routes" Is Actually Safe

It's worth stating explicitly, because it surprises people coming from frameworks with stricter routing conventions: you can put non-route files directly inside route folders in app, and nothing bad happens.

app/
└── blog/
    ├── page.tsx
    ├── PostCard.tsx        # Not a route — just a component
    ├── utils.ts            # Not a route — just a helper
    └── [slug]/
        └── page.tsx

PostCard.tsx and utils.ts are never served as routes, because only page and route files are ever routable, and even then, only the content those files explicitly return reaches the client — not the file itself. This is the mechanism that makes colocation safe, and it's why Next.js can afford to be "unopinionated" about structure: the routing system's opt-in nature means there's no structural cost to keeping a component right next to the one route that uses it.

That said, "safe" doesn't mean "always the right call" — which brings us to the actual structural decision most teams get stuck on.

Choosing a Structure: Three Real Strategies

The documentation presents three organizational patterns as roughly equivalent options. In practice, the right one depends heavily on project size and team shape, and it's worth being more opinionated about this than the docs are willing to be.

Strategy 1: Everything outside app. Keep app purely for routing — layouts, pages, route handlers — and put every shared component, hook, and utility in root-level folders like components/, lib/, and hooks/.

src/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── blog/
│       └── page.tsx
├── components/
├── lib/
└── hooks/

This is the right default for small-to-medium projects, and for teams migrating from the Pages Router, where this was already the only option. It keeps the mental model simple: "if it's in app, it's routing; if it's not, it's a shared resource."

Strategy 2: Shared code inside app, at the root. Same idea, but the shared folders live inside app itself rather than beside it.

app/
├── layout.tsx
├── page.tsx
├── components/
├── lib/
└── blog/
    └── page.tsx

Functionally near-identical to Strategy 1 — the only real difference is whether your editor's file tree groups routing and shared code together or keeps them visually separate. Pick this if your team prefers a single top-level folder to reason about.

Strategy 3: Split by feature, colocated per route. Only truly global code — a design system, a top-level <Providers> wrapper — lives at the root of app. Everything else lives inside the specific route segment that uses it.

app/
├── layout.tsx
├── _components/          # Genuinely global: Header, Footer
├── blog/
│   ├── page.tsx
│   ├── _components/      # Only used by blog routes
│   │   └── PostCard.tsx
│   └── [slug]/
│       └── page.tsx
└── dashboard/
    ├── page.tsx
    └── _components/      # Only used by dashboard routes
        └── RevenueChart.tsx

This scales the best for large, multi-team applications, because it means a team working on /dashboard never has to think about what's inside /blog's private folders, and vice versa. The tradeoff is real, though: it takes more discipline to keep "genuinely shared" code from slowly migrating into a single feature's folder just because that's where someone needed it first, and it makes a component's "correct" home a matter of judgment rather than a fixed rule.

My honest recommendation: start with Strategy 1. It's the least to think about, and for the vast majority of projects — marketing sites, blogs, small-to-mid dashboards — it never becomes a bottleneck. Only migrate to Strategy 3 once you actually feel the pain of a components/ folder with fifty unrelated files in it, or once you have more than one team working in the same codebase and stepping on each other's files. Don't adopt the most complex structure preemptively for a project that doesn't need it yet — that's optimizing for a scale problem you may never have.

The src Folder: A Decision Worth Making Early

Wrapping everything (app, components, lib) in an optional src/ folder is purely cosmetic to Next.js — it changes nothing about routing or behavior. What it does change is the top level of your repository: instead of seeing app/, components/, next.config.js, and package.json all mixed together at the root, config files stay at the root and everything else moves under src/.

This is one of the few decisions in this article that's genuinely painful to reverse on an established project — moving app in or out of src after the fact means touching every import path in the project (if you're not using path aliases) or at minimum re-running your formatter and double-checking your tsconfig.json path mappings. If you have a preference, express it in your very first commit, not your fiftieth.

Practical Notes the Docs Don't Spell Out

A few things worth knowing that don't show up as clearly in the reference docs as they do once you've actually shipped a few projects:

  • Naming conflicts are the real reason to prefix with underscores. It's tempting to skip _components in favor of a plain components folder, since colocation makes both equally "safe" today. But Next.js does occasionally add new special file conventions (recent examples: default.js, instrumentation-client.js). A plain folder name has no protection against a future convention colliding with it; an underscore-prefixed one always will.
  • tsconfig.json path aliases pay for themselves immediately. Whatever structure you choose, set up a @/* alias pointing at your src (or project root) so imports read as import { Button } from "@/components/Button" instead of ../../../components/Button. This matters more as you adopt colocation, since relative import depth becomes unpredictable once files live at varying nesting levels.
  • Route groups don't compose infinitely without cost. Nesting (group) folders inside other (group) folders is legal, but a folder tree with three or four levels of parenthesized groups becomes genuinely hard to read. If you find yourself doing this, it's usually a sign that the underlying routes should be split into a separate app entirely (see multi-zones), not organized more cleverly within one.
  • Private folders and route groups solve different problems, not the same one. A common early mistake is reaching for a route group (shared) to hide utility code, when what's actually needed is a private folder _shared. Route groups still produce navigable routes for anything inside them with a page.tsx — they only hide the folder name from the URL. Private folders hide the entire subtree from routing altogether.
  • Colocated test files work exactly as you'd hope. A page.test.tsx sitting next to page.tsx is never served, for the same reason any other colocated file isn't — only page and route are routable, and test files don't match either name.

Key Takeaways

ConventionSyntaxEffect
Private folder_folderNameOpts a subtree out of routing entirely; signals implementation detail
Route group(folderName)Organizes folders without adding a URL segment; enables per-group layouts
Dynamic segment[slug]Matches exactly one path segment as a param
Catch-all segment[...slug]Matches one or more path segments
Optional catch-all[[...slug]]Matches zero or more path segments
Parallel route slot@slotRenders independently-navigable UI alongside the main page
Intercepted route(.), (..), (..)(..), (...)Renders another route's UI inside the current layout, changing the URL without a full navigation
Colocated fileany non-special filenameNever routable, regardless of nesting depth

Next.js gives you exactly enough structure to be consistent and exactly enough freedom to avoid fighting the framework. The mistake to avoid isn't picking the "wrong" strategy out of the three above — it's picking none at all, and letting structure happen by accident one pull request at a time. Decide early, write it down somewhere your team can find it, and revisit the decision only when you actually feel a real limitation, not a hypothetical one.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper