Next.js 16 Routing Explained: A Complete Beginner's Guide (2026)
If you've ever opened a Next.js project and wondered why a folder named (marketing) doesn't show up in the URL, or what on earth @modal means in a file path — you're not alone. Routing is one of the first things you touch in Next.js, and it's also one of the most misunderstood parts of the App Router.
The good news? Once you understand the core idea — folders are routes — everything else is just variations on that one theme.
By the end of this guide, you'll understand every major routing pattern in Next.js 16: static routes, dynamic routes, nested layouts, route groups, parallel routes, intercepting routes, and catch-all segments. We'll build real examples for each one, not just theory.
| Pattern | Example | URL |
|---|---|---|
| Static | about/page.tsx | /about |
| Dynamic | [slug]/page.tsx | /post/hello |
| Catch-all | [...slug]/page.tsx | /docs/a/b |
| Optional Catch-all | [[...slug]]/page.tsx | /docs or /docs/a |
| Route Group | (marketing)/pricing/page.tsx | /pricing |
| Parallel Route | @analytics/page.tsx | Slot |
| Intercepting Route | (.)photo/[id]/page.tsx | Modal |
Why Next.js Routing Feels Confusing at First
Unlike React Router or other client-side routers, Next.js doesn't use a config file or a <Routes> component to define your pages. Instead, it uses your file system as the source of truth. The folder structure inside app/ directly maps to the URLs your site responds to.
This is powerful once it clicks, but it also means special folder names (like [slug], (group), or @slot) carry real meaning. Misreading one of these conventions is usually where beginners get stuck.
Let's go through them one at a time.
The Basics: How Folders Become URLs
In the App Router, every route lives inside the app/ directory, and a folder only becomes a visible page when it contains a page.tsx file.
// app/about/page.tsx
export default function AboutPage() {
return <h1>About Us</h1>;
}
This single file creates the route /about. No router config, no manual registration — Next.js scans the folder tree and wires it up automatically.
💡 Tip: A folder without a
page.tsxinside it does not create a route. It might just be there to hold layouts, components, or nested routes.
Special Files You'll See in a Route Folder
| File | Purpose |
|---|---|
page.tsx | Makes the route publicly accessible (the actual UI) |
layout.tsx | Wraps the page (and its children) with shared UI |
loading.tsx | Shown automatically while the page is loading |
error.tsx | Catches errors thrown inside that route segment |
not-found.tsx | Shown when notFound() is called or no route matches |
We'll use most of these as we go.
Nested Routes
Want /blog/getting-started to be a real page? Just nest folders:
app/
└── blog/
├── page.tsx → /blog
└── getting-started/
└── page.tsx → /blog/getting-started
// app/blog/getting-started/page.tsx
export default function GettingStartedPage() {
return <h1>Getting Started With Next.js</h1>;
}
Each folder level adds a segment to the URL path. There's no limit to how deep you can nest — Next.js just keeps following the folders.
Dynamic Routes
Static folders are great until you need a page per blog post, per product, or per user. That's where dynamic routes come in, using square brackets in the folder name.
app/
└── blog/
└── [slug]/
└── page.tsx → /blog/anything-here
// app/blog/[slug]/page.tsx
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = await params;
return <h1>Reading post: {slug}</h1>;
}
Visiting /blog/nextjs-routing-guide renders this page with slug equal to "nextjs-routing-guide".
⚠️ Common Mistake: In Next.js 16,
paramsis a Promise, not a plain object. You mustawaitit before reading values — forgetting this is one of the most common upgrade bugs when migrating from older Next.js versions.
Generating Pages at Build Time
If you know all possible slugs ahead of time (e.g. you're reading blog posts from the file system or a CMS), you can pre-render them at build time with generateStaticParams:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
This tells Next.js "here are all the dynamic values that exist — build a static page for each one" instead of rendering them on-demand at request time.
Catch-All and Optional Catch-All Routes
Sometimes one dynamic segment isn't enough. Maybe you want /docs/a, /docs/a/b, and /docs/a/b/c to all hit the same page, with the rest of the path available as an array. That's a catch-all route, written with three dots inside the brackets.
app/
└── docs/
└── [...slug]/
└── page.tsx
// app/docs/[...slug]/page.tsx
interface DocsPageProps {
params: Promise<{ slug: string[] }>;
}
export default async function DocsPage({ params }: DocsPageProps) {
const { slug } = await params;
// For /docs/a/b/c, slug = ["a", "b", "c"]
return <h1>Docs path: {slug.join(" / ")}</h1>;
}
There's also an optional catch-all, which uses double brackets [[...slug]]. The difference: with optional catch-all, /docs itself (with no extra segments) also matches the same page, instead of returning a 404.
app/
└── docs/
└── [[...slug]]/
└── page.tsx → matches /docs, /docs/a, /docs/a/b, etc.
💡 Tip: Catch-all routes are exactly how Clerk's
[[...sign-in]]pattern works under the hood — one page handles every sub-step of the auth flow.
Layouts: Shared UI Across Routes
A layout.tsx wraps every page inside its folder (and all nested folders below it) without losing state on navigation — things like a sidebar won't re-render or flicker when you click between pages.
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex gap-6">
<aside className="w-48">Blog Sidebar</aside>
<main className="flex-1">{children}</main>
</div>
);
}
Every page under app/blog/ — including [slug]/page.tsx — now renders inside this layout automatically. Layouts nest too: the root app/layout.tsx wraps everything, and a deeper layout wraps just its own subtree.
⚠️ Common Mistake: Layouts don't re-render when you navigate between sibling pages that share them. If you need something to reset per-page (like scroll position or a
key-based remount), you'll need to handle that explicitly — layouts are persistent by design.
Route Groups: Organizing Without Affecting the URL
Sometimes you want to organize routes into logical folders — like separating marketing pages from authenticated dashboard pages — without those folder names showing up in the URL. Wrapping a folder name in parentheses does exactly that.
app/
├── (marketing)/
│ ├── layout.tsx → navbar + footer
│ ├── page.tsx → /
│ └── pricing/
│ └── page.tsx → /pricing
│
└── (dashboard)/
├── layout.tsx → sidebar + topbar
└── settings/
└── page.tsx → /settings
Notice that /pricing doesn't become /marketing/pricing — the parentheses tell Next.js "this folder is for organization only, ignore it in the URL." This is exactly how you'd separate a public marketing layout from a private dashboard layout, each with completely different navigation chrome.
💡 Tip: Route groups are also useful for giving the same URL segment multiple layout options during development, or for splitting large apps into logical sections for your team without touching the live URL structure.
Parallel Routes: Rendering Multiple Pages at Once
This is where things get genuinely interesting. Parallel routes let you render two or more independent pages in the same layout, side by side — each with its own loading and error state. You define them with a @ prefix, called a "slot."
Imagine a dashboard that shows an analytics chart and a notifications feed at the same time, but each one loads independently:
app/
└── dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/
│ └── page.tsx
└── @notifications/
└── page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">{children}</div>
<div>{analytics}</div>
<div>{notifications}</div>
</div>
);
}
Each slot (@analytics, @notifications) is passed into the layout as a prop, matching its folder name. Next.js renders them all in parallel — if @notifications is still loading, it won't block @analytics from showing up.
⚠️ Common Mistake: Slot folders need their own
page.tsx(or adefault.tsxas a fallback) — if a slot has no matching route for the current URL and nodefault.tsx, Next.js will throw a 404 for the entire layout, not just that slot.
// app/dashboard/@notifications/default.tsx
export default function Default() {
return null; // fallback when no route matches this slot
}
Intercepting Routes: Modals That Feel Like Pages
Ever clicked a photo on Instagram and seen it open in a modal — but if you refresh that same URL, it loads as a full standalone page instead? That's an intercepting route, and Next.js supports this pattern natively using (.)-style folder prefixes.
app/
├── feed/
│ ├── page.tsx
│ └── @modal/
│ ├── default.tsx
│ └── (.)photo/
│ └── [id]/
│ └── page.tsx → intercepted modal view
└── photo/
└── [id]/
└── page.tsx → full page (direct visit / refresh)
The convention:
(.)intercepts a route at the same level(..)intercepts one level above(..)(..)intercepts two levels above(...)intercepts from the root
// app/feed/@modal/(.)photo/[id]/page.tsx
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<div className="fixed inset-0 flex items-center justify-center bg-black/50">
<div className="bg-white p-6 rounded-xl">Photo {id} (modal view)</div>
</div>
);
}
So when a user clicks a photo while browsing the feed, Next.js intercepts the navigation and shows the modal. But if they paste that same /photo/123 URL directly into a new tab, Next.js skips the interception and renders the real app/photo/[id]/page.tsx as a normal full page.
💡 Tip: Intercepting routes are almost always paired with parallel routes (the
@modalslot) — the slot is what lets the modal render on top of the feed instead of replacing it.
Linking Between Routes
All of this routing is useless without navigation. Always use Next.js's Link component instead of a plain <a> tag — it enables client-side transitions and automatic prefetching.
import Link from "next/link";
export default function Nav() {
return (
<nav>
<Link href="/blog">Blog</Link>
<Link href="/blog/nextjs-routing-guide">A Specific Post</Link>
</nav>
);
}
For programmatic navigation (e.g. redirecting after a form submits), use the useRouter hook in a Client Component:
"use client";
import { useRouter } from "next/navigation";
export default function SubmitButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/dashboard")}>
Go to Dashboard
</button>
);
}
Frequently Asked Questions
Do route groups affect performance?
No. Route groups are purely an organizational tool at build time — they don't add extra network requests, extra DOM nodes, or any runtime cost. They simply change how your files are organized without changing the URL.
Can I combine dynamic routes with route groups?
Yes, and it's common. For example, app/(dashboard)/projects/[id]/page.tsx gives you /projects/123 while keeping the dashboard layout grouping intact.
What's the difference between loading.tsx and a parallel route's own loading state?
loading.tsx shows a fallback for an entire route segment while its data loads. Inside parallel routes, each slot (@analytics, @notifications, etc.) can have its own loading.tsx, so one slot can show a spinner while another already displays its content — they load independently.
Why did my page 404 after adding a parallel route slot?
This usually means a slot (like @modal) has no page.tsx matching the current URL and no default.tsx fallback. Add a default.tsx that returns null (or some neutral fallback UI) to fix it.
Can intercepting routes work without parallel routes?
Technically you can intercept into the main children slot, but in practice almost every real-world use case (modals, lightboxes, previews) pairs intercepting routes with a parallel @slot so the intercepted content renders alongside the existing page instead of replacing it.
Wrapping Up
Next.js 16's App Router routing system might look intimidating with all its special folder syntax, but it really boils down to a handful of conventions: brackets for dynamic segments, dots for catch-alls, parentheses for invisible grouping, and @ for parallel slots. Once you've built each pattern once with your own hands, reading someone else's project structure stops feeling like decoding a puzzle.
From here, a natural next step is wiring up authentication-protected routes using these same patterns — check out the Clerk guide below to see route groups and catch-all routes used together in a real auth flow.