Skip to main content
paulund

4 min read

#nextjs#app-router#react

The Next.js App Router Mental Model

The App Router looks simple until you hit your first nested layout and a parallel route, and then it gets confusing fast. The thing that makes it click for me is realising there's a single underlying idea: the URL is a path through a tree of folders, and each folder contributes one piece of the final page.

Once you hold that in your head, everything else is a variation on the same theme.

Folders Are Routes

A folder under app/ is a route segment. app/products/shoes/page.tsx maps to /products/shoes. There is no route file to register, no manifest, no config. The directory structure is the routing table.

Inside a route segment, a small set of special files give you different capabilities:

FileWhat it does
page.tsxRenders the content at that exact URL
layout.tsxWraps the page and all nested routes; state persists across routes
loading.tsxShown via Suspense while the segment is loading
error.tsxError boundary for the segment
not-found.tsxRendered when notFound() is called or a route doesn't match
route.tsA route handler for HTTP methods (GET, POST, and so on)

The folder has either a page.tsx or a route.ts, not both. That's the only rule.

Layouts Nest, Pages Don't

This is the piece that changes how you structure an app. Layouts wrap their children, and layouts nest. A request for /dashboard/settings renders the root layout, then the dashboard layout, then the settings page, stacked like this:

app/layout.tsx             (root)
└── app/dashboard/layout.tsx
    └── app/dashboard/settings/page.tsx

Each layout receives children and decides how to render them. State inside a layout survives navigation between any of its descendant routes, because the layout component doesn't unmount when you go from /dashboard/settings to /dashboard/billing. Only the page.tsx changes.

// app/dashboard/layout.tsx
export default function DashboardLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <div className="dashboard">
            <Sidebar />
            <main>{children}</main>
        </div>
    )
}

That <Sidebar /> doesn't re-render when the user clicks between sub-pages. The layout is the stable shell, the page is the thing that changes.

Route Groups: Folders That Don't Affect the URL

A folder wrapped in parentheses is a route group. It organises files without adding a segment to the URL:

app/
├── (marketing)/
│   ├── layout.tsx
│   ├── page.tsx          -> /
│   └── about/page.tsx    -> /about
└── (app)/
    ├── layout.tsx
    └── dashboard/page.tsx -> /dashboard

Two route groups, two different layouts, no URL change. I use these whenever a project has genuinely different shells (a public marketing site and an authenticated app, for example) but still lives under the same Next.js project.

Dynamic Segments

Square brackets mark a dynamic segment. app/notebook/[section]/[slug]/page.tsx matches /notebook/react/composition-over-prop-drilling and passes section and slug as params:

type Params = { section: string; slug: string }

export default async function NotePage({
    params,
}: {
    params: Promise<Params>
}) {
    const { section, slug } = await params
    const note = await getNote(section, slug)
    return <article>{note.content}</article>
}

In Next.js 15, params is a Promise. You await it. The reason is that the router wants to start rendering before the params are resolved when it can, and forcing you to await makes that boundary explicit.

For a static export, you pair this with generateStaticParams() to tell Next.js every concrete path it should build:

export function generateStaticParams() {
    return getAllNotes().map((n) => ({
        section: n.section,
        slug: n.slug,
    }))
}

The Request Lifecycle

When a request comes in, here's what happens, conceptually:

  1. Next.js matches the URL to a folder path under app/.
  2. It collects every layout above the target page and every loading/error boundary along the way.
  3. Server Components render top-down. The root layout runs, then nested layouts, then the page. Data fetching inside them is deduplicated automatically.
  4. The rendered result streams to the browser. Any Suspense boundaries (including loading.tsx) get their fallbacks first, then the real content flushes as it becomes ready.
  5. In the browser, Client Components hydrate. The interactive parts wake up.

Navigations to sibling routes are cheap. Layouts stay mounted, shared data stays cached, only the changed page re-renders.

What to Hold in Your Head

When a URL doesn't work the way you expect, walk the folder tree. What layouts are wrapping it? What's the closest error.tsx? Is there a loading.tsx that should exist but doesn't? The mental model is small, but you have to apply it before you can debug. Most App Router bugs I've hit came from forgetting that a layout above the page was doing something I'd stopped thinking about.

Related notes


Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.