Skip to main content

Every Next.js routing pattern you need to know

A complete guide to Next.js App Router routing — static, dynamic, slug, catch-all, parallel, intercepting, and more. With real examples.

––– views

Next.js has one of the most powerful file-based routing systems in the frontend world. You create a file, you get a route. But there's a lot more depth to it than that — dynamic segments, catch-all routes, parallel routes, intercepting routes, and more.

This post covers every routing pattern in the App Router (Next.js 13+) with real examples so you know exactly when and how to use each one.


1. Static routing

The simplest kind. Create a file inside app/, and the path maps directly to the URL.

app/
├── page.tsx          → /
├── about/
│   └── page.tsx      → /about
└── contact/
    └── page.tsx      → /contact
// app/about/page.tsx
export default function AboutPage() {
  return <h1>About us</h1>
}

That's it. No configuration. The folder name becomes the URL segment.


2. Dynamic routing — [param]

When you don't know the path ahead of time — like a user profile or a product page — use a dynamic segment by wrapping the folder name in square brackets.

app/
└── users/
    └── [id]/
        └── page.tsx   → /users/1, /users/42, /users/abc
// app/users/[id]/page.tsx
type Props = {
  params: { id: string }
}
 
export default function UserPage({ params }: Props) {
  return <h1>User ID: {params.id}</h1>
}

The value in the URL becomes available via params. So visiting /users/99 gives you params.id === "99".

Fetching data with dynamic params

// app/users/[id]/page.tsx
async function getUser(id: string) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
  return res.json()
}
 
export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await getUser(params.id)
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

3. Slug routing — [slug]

A slug is just a dynamic segment with a semantic name. Typically used for blog posts, articles, or any human-readable URL.

app/
└── blog/
    └── [slug]/
        └── page.tsx   → /blog/my-first-post, /blog/nextjs-routing-guide
// app/blog/[slug]/page.tsx
type Props = {
  params: { slug: string }
}
 
async function getPost(slug: string) {
  // fetch from your CMS, MDX files, DB, etc.
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  return res.json()
}
 
export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug)
 
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.description}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Generating static pages with generateStaticParams

If you want Next.js to pre-render all blog posts at build time:

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
 
  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }))
}

Next.js will call generateStaticParams, get all slugs, and pre-render each one as a static page. Fast and SEO-friendly.


4. Nested dynamic routing

You can nest dynamic segments for more complex URL structures.

app/
└── blog/
    └── [category]/
        └── [slug]/
            └── page.tsx   → /blog/javascript/understanding-closures
// app/blog/[category]/[slug]/page.tsx
type Props = {
  params: {
    category: string
    slug: string
  }
}
 
export default async function BlogPost({ params }: Props) {
  const { category, slug } = params
 
  return (
    <div>
      <span>Category: {category}</span>
      <h1>{slug}</h1>
    </div>
  )
}

Visiting /blog/javascript/understanding-closures gives:

  • params.category === "javascript"
  • params.slug === "understanding-closures"

5. Catch-all routes — [...slug]

A catch-all segment matches any number of path segments from that point on.

app/
└── docs/
    └── [...slug]/
        └── page.tsx

This single file handles all of:

  • /docs/intro
  • /docs/guides/installation
  • /docs/api/reference/components
// app/docs/[...slug]/page.tsx
type Props = {
  params: { slug: string[] }
}
 
export default function DocsPage({ params }: Props) {
  // params.slug is an array of segments
  // /docs/guides/installation → ['guides', 'installation']
 
  return (
    <div>
      <p>Path: {params.slug.join(' / ')}</p>
    </div>
  )
}

params.slug is always an array with catch-all routes — not a string.


6. Optional catch-all routes — [[...slug]]

Same as catch-all, but also matches the root path with no segments.

app/
└── shop/
    └── [[...slug]]/
        └── page.tsx

This handles:

  • /shopparams.slug === undefined
  • /shop/shoesparams.slug === ['shoes']
  • /shop/shoes/nikeparams.slug === ['shoes', 'nike']
// app/shop/[[...slug]]/page.tsx
type Props = {
  params: { slug?: string[] }
}
 
export default function ShopPage({ params }: Props) {
  if (!params.slug) {
    return <h1>All Products</h1>
  }
 
  return <h1>Category: {params.slug.join(' > ')}</h1>
}

The difference between [...slug] and [[...slug]]:

  • [...slug]/shop returns 404, /shop/anything works
  • [[...slug]] — both /shop and /shop/anything work

7. Route groups — (group)

Route groups let you organize files without affecting the URL. Wrap a folder name in parentheses and it's invisible to the router.

app/
└── (marketing)/
    ├── about/
    │   └── page.tsx    → /about
    └── pricing/
        └── page.tsx    → /pricing
└── (dashboard)/
    ├── settings/
    │   └── page.tsx    → /settings
    └── analytics/
        └── page.tsx    → /analytics

This is great for:

  • Separate layouts — marketing pages get a different layout than dashboard pages
  • Code organization — group related routes without polluting the URL
// app/(dashboard)/layout.tsx — only applies to dashboard routes
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

8. Parallel routes — @slot

Parallel routes let you render multiple pages simultaneously in the same layout — like a dashboard with independent panels that each have their own loading/error states.

app/
└── dashboard/
    ├── @analytics/
    │   └── page.tsx
    ├── @team/
    │   └── page.tsx
    └── layout.tsx
// app/dashboard/layout.tsx
type Props = {
  analytics: React.ReactNode
  team: React.ReactNode
}
 
export default function DashboardLayout({ analytics, team }: Props) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
      <section>{analytics}</section>
      <section>{team}</section>
    </div>
  )
}

Each slot (@analytics, @team) renders its own page.tsx independently. If one is loading or errors, it doesn't affect the other.


9. Intercepting routes — (.), (..), (...)

Intercepting routes let you load a route inside the current layout without a full page navigation. The classic example: opening a photo in a modal when clicked in a feed, but navigating directly to the full photo page when you refresh.

app/
└── feed/
    ├── page.tsx
    ├── (.)photo/
    │   └── [id]/
    │       └── page.tsx   ← intercepts /photo/[id] when navigating from /feed
    └── photo/
        └── [id]/
            └── page.tsx   ← shown on direct visit or refresh

The notation:

  • (.) — intercepts a segment at the same level
  • (..) — intercepts a segment one level up
  • (..)(..) — intercepts two levels up
  • (...) — intercepts from the root
// app/feed/(.)photo/[id]/page.tsx — shown as a modal
import { Modal } from '@/components/modal'
 
export default function PhotoModal({ params }: { params: { id: string } }) {
  return (
    <Modal>
      <img src={`/photos/${params.id}.jpg`} alt="Photo" />
    </Modal>
  )
}
// app/photo/[id]/page.tsx — shown on direct visit
export default function PhotoPage({ params }: { params: { id: string } }) {
  return (
    <div>
      <img src={`/photos/${params.id}.jpg`} alt="Photo" />
    </div>
  )
}

10. Private folders — _folder

Prefix a folder with _ and Next.js completely ignores it for routing. Useful for colocating components, utils, or tests next to your routes.

app/
└── blog/
    ├── _components/
    │   └── PostCard.tsx   ← not a route, just a component
    ├── [slug]/
    │   └── page.tsx
    └── page.tsx

Quick reference

PatternExampleMatches
Staticapp/about/page.tsx/about
Dynamicapp/users/[id]/page.tsx/users/1
Slugapp/blog/[slug]/page.tsx/blog/my-post
Nested dynamicapp/blog/[cat]/[slug]/page.tsx/blog/js/closures
Catch-allapp/docs/[...slug]/page.tsx/docs/a/b/c
Optional catch-allapp/shop/[[...slug]]/page.tsx/shop, /shop/a/b
Route groupapp/(marketing)/about/page.tsx/about
Parallelapp/dash/@analytics/page.tsxslot in layout
Interceptingapp/feed/(.)photo/[id]/page.tsxmodal intercept
Private folderapp/blog/_components/not a route

Next.js routing looks simple on the surface but has a surprising amount of depth. Static routes get you 80% of the way. Dynamic segments and slugs cover most real-world apps. Catch-all routes handle documentation and nested taxonomies. And parallel + intercepting routes unlock UI patterns that used to require a lot of custom state management.

Start simple, reach for the advanced patterns only when you need them.