LearnFrenzy
Sign in Take a free test

Layouts and Routing · Lesson 5 of 24

Next.js Dynamic Routes and generateStaticParams

6 min read Updated Nov 8, 2026 Saurabh Samir

A course platform has hundreds of course pages, a shop thousands of product pages – all with the same layout and different data. Instead of a folder per item, you create one dynamic segment by wrapping a folder name in square brackets: app/courses/[slug]/page.tsx matches /courses/react, /courses/nextjs and so on.

In this lesson

  • Dynamic segments and the params prop
  • Prerendering pages with generateStaticParams
  • Showing a 404 for unknown slugs
  • Catch-all and optional catch-all routes

A dynamic course page

We'll keep the data in a small module – in a real app this would query a database or CMS:

// lib/courses.ts
export type Course = { slug: string; title: string; lessons: number; level: string };

const courses: Course[] = [
  { slug: "react", title: "React.js", lessons: 31, level: "Beginner to Pro" },
  { slug: "nextjs", title: "Next.js", lessons: 24, level: "Intermediate" },
  { slug: "typescript", title: "TypeScript", lessons: 17, level: "Intermediate" },
];

export async function getCourses() {
  return courses;
}

export async function getCourse(slug: string) {
  return courses.find((c) => c.slug === slug) ?? null;
}
// app/courses/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getCourse, getCourses } from "@/lib/courses";

// Prerender these pages at build time
export async function generateStaticParams() {
  const courses = await getCourses();
  return courses.map((c) => ({ slug: c.slug }));
}

export default async function CoursePage({ params }: PageProps<"/courses/[slug]">) {
  const { slug } = await params;                 // params is a Promise
  const course = await getCourse(slug);
  if (!course) notFound();                       // renders the 404 page

  return (
    <main>
      <h1>{course.title}</h1>
      <p>{course.lessons} lessons · {course.level}</p>
    </main>
  );
}
// app/page.tsx
import Link from "next/link";
import { getCourses } from "@/lib/courses";

export default async function Home() {
  const courses = await getCourses();
  return (
    <ul>
      {courses.map((c) => (
        <li key={c.slug}><Link href={`/courses/${c.slug}`}>{c.title}</Link></li>
      ))}
      <li><Link href="/courses/vue">Vue (no such course)</Link></li>
    </ul>
  );
}

Build output and visits

Route (app)
┌ ○ /
└   /courses/[slug]
  ├ ◐ /courses/[slug]
  ├ ○ /courses/react
  ├ ○ /courses/nextjs
  └ ○ /courses/typescript


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

URL: /
• React.js
• Next.js
• TypeScript
• Vue (no such course)

› click “Next.js”
URL: /courses/nextjs
Next.js
24 lessons · Intermediate

› open /courses/vue
URL: /courses/vue
404
This page could not be found.
  • params is a Promise – await it in async components. PageProps<"/courses/[slug]"> is a type Next.js generates from your folders, so slug is typed as a string automatically.
  • generateStaticParams returns the slugs to build ahead of time. The build output lists each prerendered page under the route.
  • notFound() stops rendering and shows the nearest not-found.tsx (or the default 404 page), and tells search engines not to index it. The last lesson in this chapter explains the HTTP status it gets.
  • Slugs not returned by generateStaticParams are still rendered on demand the first time they're requested.

Without generateStaticParams

If you can't list the values in advance, the params are only known when a request arrives. With Cache Components, code that waits for request-time data must be wrapped in <Suspense>, so Next.js can still send the rest of the page instantly:

// app/orders/[id]/page.tsx
import { Suspense } from "react";

async function OrderDetails({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <p>Order #{id}: 2 items, shipped</p>;
}

export default function OrderPage({ params }: PageProps<"/orders/[id]">) {
  return (
    <main>
      <h1>Your order</h1>
      <Suspense fallback={<p>Loading order…</p>}>
        <OrderDetails params={params} />
      </Suspense>
    </main>
  );
}

Build output and page

Route (app)
└   /orders/[id]
  └ ◐ /orders/[id]


○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

Your order
Order #4521: 2 items, shipped

The route is marked ◐ Partial Prerender: the heading and fallback form a static shell built at deploy time, and the order details are filled in per request. If you forget the Suspense, next build refuses to continue and tells you how to fix it:

// app/orders/[id]/page.tsx
export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
  const { id } = await params;          // request-time data, no Suspense around it
  return <H2>Order #{id};
}

next build

Error: Route "/orders/[id]": Next.js encountered uncached or runtime data during prerendering.
`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
  - [block] Set `export const instant = false` to allow a blocking route
Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
Error occurred prerendering page "/orders/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /orders/[id]/page: /orders/[id], exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null

The three fixes it lists are the three tools of Next.js 16 rendering: stream it behind Suspense, cache it with "use cache", or deliberately allow a blocking route. You'll use all three in this course.

Catch-all segments

FolderMatchesparams
[slug]/docs/intro{ slug: "intro" }
[...slug]/docs/a, /docs/a/b/c (not /docs){ slug: ["a", "b", "c"] }
[[...slug]]the same, and /docs{ slug: undefined } for /docs
// app/docs/[[...slug]]/page.tsx
export function generateStaticParams() {
  return [{ slug: [] }, { slug: ["routing"] }, { slug: ["routing", "dynamic-routes"] }];
}

export default async function Docs({ params }: PageProps<"/docs/[[...slug]]">) {
  const { slug = [] } = await params;
  return (
    <main>
      <p>Docs / {slug.join(" / ") || "home"}</p>
      <p>{slug.length} segment(s)</p>
    </main>
  );
}

Visits

Docs / home
0 segment(s)

› open /docs/routing/dynamic-routes
Docs / routing / dynamic-routes
2 segment(s)

Practice

Try it yourself

  1. Create /blog/[slug] for two posts with generateStaticParams, showing the slug converted to a title ("my-first-post" → "My first post").

    Show solution
    // app/blog/[slug]/page.tsx
    export function generateStaticParams() {
      return [{ slug: "my-first-post" }, { slug: "learning-nextjs" }];
    }
    
    export default async function Post({ params }: PageProps<"/blog/[slug]">) {
      const { slug } = await params;
      const title = slug.replaceAll("-", " ");
      return <h1>{title.charAt(0).toUpperCase() + title.slice(1)}</h1>;
    }

    Build output and page

    Route (app)
    └   /blog/[slug]
      ├ ◐ /blog/[slug]
      ├ ○ /blog/my-first-post
      └ ○ /blog/learning-nextjs
    
    
    ○  (Static)             prerendered as static content
    ◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
    
    My first post

Quick check

How do you read params in a Next.js 16 page?

const { slug } = await params; – params is a Promise.

What does generateStaticParams do?

Returns the param values to prerender at build time.

What's the difference between [...slug] and [[...slug]]?

The optional version also matches the route without any segments.

Summary

  • [slug] folders create dynamic routes; await the params Promise to read them.
  • generateStaticParams prerenders known pages; others render on demand.
  • Call notFound() for missing data; wrap request-time params in Suspense when they aren't prerendered.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular