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.
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.
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 syntax | Matches | Example |
|---|---|---|
[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 } |