Pages and Routing

File-based routing with the App Router, nested layouts, dynamic segments, and linking with next/link.

File-based routing, in practice

Each segment of a URL corresponds to a folder in app/, and a page.tsx file inside a folder is what makes that segment a navigable page rather than just an organizational folder:

Typescript
// app/page.tsx  →  the "/" route
export default function HomePage() {
  return <h1>Welcome home</h1>;
}
Typescript
// app/about/page.tsx  →  the "/about" route
export default function AboutPage() {
  return <h1>About us</h1>;
}

Layouts

A layout.tsx file wraps every page (and every nested layout) beneath it in the folder tree, and — critically — persists across navigations between sibling pages instead of re-rendering from scratch:

Typescript
// app/layout.tsx — the root layout, required, wraps the entire app
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav>My Site</nav>
        {children}
      </body>
    </html>
  );
}
Typescript
// app/blog/layout.tsx — wraps every page under /blog only
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="blog-layout">
      <aside>Blog sidebar</aside>
      <main>{children}</main>
    </div>
  );
}

Layouts nest: a page at app/blog/[slug]/page.tsx renders inside BlogLayout, which itself renders inside RootLayout.

Dynamic segments

Wrapping a folder name in square brackets creates a dynamic segment that matches any value at that position in the URL:

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

/blog/hello-world renders this page with slug equal to "hello-world". As of Next.js 15, params (and searchParams) are provided as a Promise that must be awaited — this changed from earlier versions, where params was a plain object, to support Next's async request APIs consistently.

Two further variants exist for matching more than one segment:

  • app/shop/[...slug]/page.tsx — a catch-all segment, matching /shop/a, /shop/a/b, /shop/a/b/c, with slug as an array.
  • app/shop/[[...slug]]/page.tsx — an optional catch-all, which additionally matches /shop itself (empty array), not just deeper paths.

Linking between pages

Use next/link instead of a plain <a> tag — it enables client-side navigation (no full page reload) and automatically prefetches the linked page's code when it scrolls into view:

Typescript
import Link from 'next/link';

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog/hello-world">A specific post</Link>
    </nav>
  );
}

Common mistakes

  • Using a plain <a href="/about"> instead of <Link> — it still works, but forces a full page reload and loses Next's prefetching, defeating much of the point of client-side routing.
  • Forgetting to await params in Next.js 15+ — treating it as a plain object throws a runtime error, since it's now a Promise.
  • Naming a folder [slug] when the route actually needs to match multiple segments — that requires the [...slug] (or [[...slug]]) catch-all syntax instead.