R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 14 of 42Article16 min

Dynamic, Catch-all & Optional Catch-all Routes

A folder named [id] matches one segment: /courses/101 → params.id is "101". Use this for blog posts, products, and user profiles.

Dynamic segment — [id]

A folder named [id] matches one segment: /courses/101 → params.id is "101". Use this for blog posts, products, and user profiles.

In App Router, page props receive params (often as a Promise in newer Next.js versions — await params when your version requires it).

Real-life example: [id] is a hotel room number. The hallway shape is the same; the room number changes which door you open.

Tip: Name the bracket after what it means — [slug], [userId], [courseId] — so params are easy to read.
app/courses/[id]/page.tsx
type Props = {
  params: Promise<{ id: string }>;
};

export default async function CoursePage({ params }: Props) {
  const { id } = await params;

  // Example: fetch course by id
  // const course = await getCourse(id);

  return (
    <article>
      <h1>Course #{id}</h1>
      <p>Dynamic route for one Rishtaara course.</p>
    </article>
  );
}
Folder + Link
app/courses/[id]/page.tsx   →  /courses/abc
                             →  /courses/nextjs-basics

# Link example:
# <Link href={`/courses/${course.id}`}>{course.title}</Link>

Catch-all — [...slug]

[...slug] captures one or more segments as an array. /docs/a/b/c → slug = ["a", "b", "c"]. It does not match the parent path alone unless you also add a page there.

Great for docs trees, nested categories, and file-path style URLs.

Real-life example: Catch-all is a suitcase that holds every remaining layer of clothing — one bag for the rest of the trip itinerary.

app/docs/[...slug]/page.tsx
type Props = {
  params: Promise<{ slug: string[] }>;
};

export default async function DocsPage({ params }: Props) {
  const { slug } = await params;
  const path = slug.join("/");

  return (
    <article>
      <h1>Docs: /{path}</h1>
      <ol>
        {slug.map((part) => (
          <li key={part}>{part}</li>
        ))}
      </ol>
    </article>
  );
}

// Matches:
// /docs/getting-started
// /docs/nextjs/routing/dynamic
// Does NOT match /docs alone (add app/docs/page.tsx for that)

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

Double brackets [[...slug]] also match the route with zero segments. So app/shop/[[...slug]]/page.tsx can serve /shop and /shop/shoes/red.

When there are no segments, slug may be undefined — always guard for that.

Real-life example: Optional catch-all is a bus that stops at the depot with no passengers and also stops at every town along the route.

Tip: Prefer a simple [id] when you only need one value. Use catch-all only when the depth of the path is unknown.
app/shop/[[...slug]]/page.tsx
type Props = {
  params: Promise<{ slug?: string[] }>;
};

export default async function ShopPage({ params }: Props) {
  const { slug } = await params;

  if (!slug || slug.length === 0) {
    return <h1>All products — Rishtaara Shop</h1>;
  }

  return (
    <h1>
      Category: {slug.join(" / ")}
    </h1>
  );
}

// Matches:
// /shop
// /shop/books
// /shop/books/programming
Compare the three
[id]         → exactly one segment
[...slug]    → one or more segments (required)
[[...slug]]  → zero, one, or more segments (optional)