Next.js Cheatsheet

Dynamic Routes

Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Dynamic Segment Syntax

Folder nameURL matchparams shape
[id]/posts/42{ id: '42' }
[slug]/blog/my-post{ slug: 'my-post' }
[...segments]/docs/a/b/c{ segments: ['a', 'b', 'c'] }
[[...segments]]/docs OR /docs/a/b{ segments: [] } or { segments: ['a', 'b'] }

Single Dynamic Segment

// app/blog/[slug]/page.tsx  →  /blog/anything
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetchPost(slug)

  return <article>{post.body}</article>
}

Catch-All Segment [...slug]

Matches one or more path segments. The segment is required/docs alone returns 404.

// app/docs/[...slug]/page.tsx  →  /docs/a   /docs/a/b   /docs/a/b/c
export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug: string[] }>
}) {
  const { slug } = await params
  // slug = ['a']  OR  ['a', 'b']  OR  ['a', 'b', 'c']

  const path = slug.join('/')
  return <div>Path: {path}</div>
}

Optional Catch-All [[...slug]]

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

// app/docs/[[...slug]]/page.tsx  →  /docs   /docs/a   /docs/a/b
export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  // slug = undefined  OR  ['a']  OR  ['a', 'b']

  if (!slug) return <DocsHome />
  return <DocsPage path={slug.join('/')} />
}

generateStaticParams — Pre-generate at Build

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetchAllPosts()

  return posts.map(post => ({
    slug: post.slug,           // must match the segment name
  }))
}

Multi-segment:

// app/[lang]/[slug]/page.tsx
export async function generateStaticParams() {
  return [
    { lang: 'en', slug: 'hello' },
    { lang: 'fr', slug: 'bonjour' },
  ]
}

Catch-all:

// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
  return [
    { slug: ['getting-started'] },
    { slug: ['api', 'reference'] },
    { slug: ['api', 'reference', 'hooks'] },
  ]
}

dynamicParams — Fallback for Unknown Params

// page.tsx or layout.tsx
export const dynamicParams = true    // (default) render on-demand for unknown params
export const dynamicParams = false   // 404 for any param not in generateStaticParams

Multiple Dynamic Segments

app/[org]/[repo]/page.tsx   →  /vercel/next.js
export default async function RepoPage({
  params,
}: {
  params: Promise<{ org: string; repo: string }>
}) {
  const { org, repo } = await params
  return <div>{org}/{repo}</div>
}

Dynamic Segments in Layouts

Layouts receive params too:

// app/blog/[slug]/layout.tsx
export default async function PostLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetchPost(slug)

  return (
    <div>
      <header>{post.title}</header>
      {children}
    </div>
  )
}

Combining with searchParams

// app/search/page.tsx  →  /search?q=next&page=2
export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; page?: string }>
}) {
  const { q = '', page = '1' } = await searchParams
  const results = await search(q, Number(page))
  return <ResultsList results={results} />
}

Client-side Access to Dynamic Params

'use client'
import { useParams, useSearchParams } from 'next/navigation'

export function PostActions() {
  const params = useParams<{ slug: string }>()
  const searchParams = useSearchParams()

  const slug = params.slug
  const ref = searchParams.get('ref')

  return <div>{slug} — referred by {ref}</div>
}

Generating Metadata Dynamically

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await fetchPost(slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.ogImage],
    },
  }
}

notFound() for Missing Dynamic Resources

import { notFound } from 'next/navigation'

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await db.posts.findUnique({ where: { slug } })

  if (!post) notFound()   // renders the nearest not-found.tsx

  return <article>{post.body}</article>
}

Route Collision Rules

Static segments take priority over dynamic ones:

app/blog/featured/page.tsx   →  /blog/featured  (static wins)
app/blog/[slug]/page.tsx     →  /blog/:slug     (only non-"featured" slugs)

Specificity order (high → low): exact > dynamic [segment] > catch-all [...slug] > optional catch-all [[...slug]].

TypeScript: params is a Promise (Next 15+)

// WRONG (Next 14 style — type error in Next 15)
export default function Page({ params }: { params: { slug: string } }) {
  return <div>{params.slug}</div>
}

// CORRECT (Next 15+)
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  return <div>{slug}</div>
}