Routing in Next.js Made Simple: files, links, and layouts

Posted November 17, 2025 by Karol Polakowski

Next.js routing is deliberately opinionated: file-system based routes map to URLs, Link components provide client navigation, and the app directory introduces powerful nested layouts. This article walks through core concepts and practical examples so you can build predictable, fast routes without surprises.


How Next.js routing works (high level)

Next.js maps files and folders in your project to URLs. There are two routing systems in active use:

  • pages directory (legacy but still supported): each file under pages becomes a route. Useful for smaller or existing projects.
  • app directory (recommended for new apps): nested folders and layout files enable hierarchical UI and server-first rendering by default.

Routes are evaluated by the filesystem tree, so thinking in folders and components is the fastest way to reason about your site structure.

File-system routing: pages vs app

pages directory example (pages-based routing):

pages/
  _app.tsx
  index.tsx        # -> /
  about.tsx        # -> /about
  blog/
    index.tsx      # -> /blog
    [slug].tsx  # -> /blog/:slug

app directory example (app-based routing):

app/
  layout.tsx       # shared layout for all routes
  page.tsx         # -> /
  about
    page.tsx       # -> /about
  blog/
    layout.tsx     # nested layout for /blog/*
    page.tsx       # -> /blog
    [slug]/
      page.tsx     # -> /blog/:slug

Key differences:

  • app uses Server Components by default; pages uses only React components (can be server-side, static, or client depending on data fetching APIs).
  • app provides nested layouts and better primitives for streaming and edge rendering.

Dynamic routes and catch-all routes

Dynamic segments let you model variable URL parts. In the app directory the filename is a folder named with brackets.

Example: a dynamic blog post page

// app/blog/[slug]/page.tsx
import React from "react";

type Props = { params: { slug: string } };

export default async function PostPage({ params }: Props) {
  const { slug } = params;
  // server-side fetch
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Catch-all routes capture multiple path segments using three dots:

  • Single-segment dynamic: \[id\]
  • Catch-all: \[…slug\]
  • Optional catch-all: \[…[…slug]\] (app supports optional too)

Example of a catch-all route folder:

app/docs/[...slug]/page.tsx  # /docs/a, /docs/a/b, /docs/a/b/c

Layouts and nested layouts (app directory)

Layouts are the game-changer in the app directory: place a layout.tsx in any folder to wrap all descendant routes. Layouts enable shared UI (headers, sidebars, auth checks) while keeping routing simple.

Top-level layout:

// app/layout.tsx
import "./globals.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <header><nav>My site</nav></header>
        <main>{children}</main>
        <footer>© My Site</footer>
      </body>
    </html>
  );
}

Nested layout for blog:

// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="blog-layout">
      <aside><BlogSidebar/></aside>
      <section>{children}</section>
    </div>
  );
}

Layouts are hierarchical and compose automatically. This makes it easy to have a persistent header while swapping nested content for each route.

Linking and navigation

Client-side navigation in Next.js uses the Link component. In the app directory, you can use Link the same way as in pages, but remember Link performs client-side transitions (faster) and preserves layout state.

Simple Link example:

// components/Nav.tsx
import Link from "next/link";

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  );
}

Prefetching: Next.js prefetches pages linked with Link when in the viewport (production only). You can disable prefetch with prefetch={false}.

Programmatic navigation

In the app directory, use the next/navigation hook for client-side imperative navigation:

// components/SearchButton.tsx
"use client";

import { useRouter } from "next/navigation";

export default function SearchButton() {
  const router = useRouter();

  function goToResults(query: string) {
    router.push(`/search?q=${encodeURIComponent(query)}`);
  }

  return (
    <button onClick={() => goToResults("react")}>Search "react"</button>
  );
}

Notes:

  • In pages directory, use useRouter from next/router instead.
  • router.push triggers a client-side transition and preserves layout state.

Route groups and advanced patterns

Route groups let you organize folders without affecting the URL. Use parentheses to create logical groups:

app
  (marketing)
    about
      page.tsx    # -> /about
  (dashboard)
    settings
      page.tsx    # -> /settings

The group names (marketing, dashboard) are not part of the URL — they only help structure code and share layouts.

Other advanced patterns:

  • Intercepting routes for modals (using the \(name\) pattern with segment intercepts).
  • Parallel routes for rendering multiple UI branches simultaneously.

These are powerful but add complexity — use them when you need persistent UI regions or modal routes.

Redirects, rewrites, and middleware

  • Redirects and rewrites are configured in next.config.js or using the new redirects() and rewrites() functions in app directory. Use redirects for permanent/temporary URL changes and rewrites to mask upstream routes.
  • Middleware runs before routing on the Edge and can be used for auth, A/B tests, or geolocation-based routing.

Simple redirect in next.config.js:

// next.config.js
module.exports = {
  async redirects() {
    return [
      { source: '/old-blog/:slug*', destination: '/blog/:slug*', permanent: true },
    ];
  },
};

Best practices

  • Prefer the app directory for new projects to get layouts, server components, and streaming.
  • Keep routes small and focused. Avoid deeply nested routes unless the UI needs it.
  • Use dynamic segments for resource IDs and catch-all for nested content like docs or CMS pages.
  • Use Link for navigation and router.push for programmatic flows.
  • Leverage layouts to colocate UI and data fetching for each part of the app.
  • Keep client components minimal: only add “use client” when you need interactivity.

Conclusion

Next.js routing is powerful because it mirrors your file structure — you get predictable URLs, nested layouts, and both server- and client-side routing primitives. Start with the app directory model: mind your layouts, use Link for client navigation, and reach for route groups and middleware only when necessary. With these patterns, routing remains simple and scalable as your app grows.