CodeOath
← All posts
Next.js31 min total · 12 parts

Next.js Fundamentals: The App Router, Server Components, and the Caching Layer Nobody Warns You About

Part 5 of 12 · ~3 min

Routing & Layouts

The App Router uses folders as route segments — there's no central route configuration file to maintain by hand. A folder's name becomes a URL segment, and a special file inside it (page.tsx) makes that segment an actual navigable route:

app/
  page.tsx              → /
  about/
    page.tsx             → /about
  blog/
    page.tsx             → /blog
    [slug]/
      page.tsx           → /blog/hello-world  (dynamic segment)

A folder alone doesn't create a route — it needs a page.tsx inside it to actually be reachable. This is deliberate: it lets you colocate other files (components, tests, utilities specific to that route) inside a route's folder without accidentally exposing them as URLs.

Layouts, and what "nested" actually buys you

A layout.tsx file wraps every page.tsx (and every nested layout) beneath it in the folder tree, and it takes a children prop for whatever it's wrapping:

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

Every route under app/dashboard//dashboard, /dashboard/settings, /dashboard/settings/billing — renders inside this shell automatically, with no need to import or repeat it per page. Nesting a layout.tsx inside app/dashboard/settings/ would add a second layer wrapping just that subtree, composing with the parent layout rather than replacing it.

The actual payoff isn't just "shared UI, written once" — it's that a layout does not re-render, re-fetch, or reset its own state on navigation between routes it wraps. Click from /dashboard to /dashboard/settings and <Sidebar /> isn't torn down and rebuilt — the same component instance persists, so a useState inside it (say, which section is expanded) survives the navigation, and any data it already fetched isn't refetched. Only the page.tsx content actually being swapped re-renders. This is a genuinely different experience from a plain single-page React app re-rendering the whole tree on every route change, and it's the reason a persistent sidebar or a music player that keeps playing across navigation is dramatically simpler to build correctly in the App Router than by hand.

Dynamic segments and catch-all segments

A folder named in square brackets — [slug] — captures whatever value sits at that position in the URL and hands it to the page as a param:

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Post: {params.slug}</h1>; // /blog/hello-world → params.slug === "hello-world"
}

A catch-all segment[...slug] — captures an arbitrary number of remaining path segments as an array instead of exactly one:

// app/docs/[...slug]/page.tsx
export default function DocsPage({ params }: { params: { slug: string[] } }) {
  // /docs/a/b/c → params.slug === ["a", "b", "c"]
  return <p>{params.slug.join(" / ")}</p>;
}

Wrapping it in a second pair of brackets — [[...slug]] — makes it optional, so the route also matches the base path with zero segments (/docs itself, with params.slug as undefined), which a plain catch-all would not match at all.

Segment syntaxMatchesExample
[id]Exactly one segment/products/42{ id: "42" }
[...slug]One or more segments/docs/a/b{ slug: ["a", "b"] }
[[...slug]]Zero or more segments/docs{ slug: undefined }