Dynamic routes in Next.js 15 — params, catch-all, and examples

Learn how dynamic routes work in Next.js 15 with the App Router. Understand [slug], [...params], params as Promises, and see code examples for blogs, profiles, and dashboards.

What are dynamic routes?

Dynamic routes let one page handle many URLs. Instead of creating app/blog/article-1/page.tsx, app/blog/article-2/page.tsx, etc., you create one file: app/blog/[slug]/page.tsx. The [slug] part captures whatever is in the URL.


Step 1: Basic dynamic route

Create app/blog/[slug]/page.tsx:

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

  return <h1>Blog post: {slug}</h1>;
}

Important for Next.js 15: params is now a Promise. You must use await params. In Next.js 14 it was a plain object — this is one of the biggest breaking changes.


Step 2: Generate metadata dynamically

Dynamic metadata
// Same file — add generateMetadata
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  // Look up the article data
  const article = articles.find((a) => a.slug === slug);

  return {
    title: article?.title ?? "Blog",
    description: article?.description,
  };
}

This gives each blog post its own title and description in Google search results, which is crucial for SEO.


Step 3: Catch-all routes

Use [...slug] to match multiple path segments:

Catch-all route
// app/docs/[...slug]/page.tsx
// Matches: /docs/getting-started
//          /docs/api/authentication
//          /docs/api/payments/stripe

export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug: string[] }>;
}) {
  const { slug } = await params;
  // slug is an array: ["api", "payments", "stripe"]

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

Step 4: Real-world example

Here's how a user profile page looks in practice:

User profile page
// app/user/[id]/page.tsx
import { createClient } from "@/libs/supabase/server";
import { notFound } from "next/navigation";

export default async function UserProfile({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const supabase = await createClient();

  const { data: profile } = await supabase
    .from("profiles")
    .select("full_name, bio")
    .eq("id", id)
    .single();

  if (!profile) {
    notFound(); // Shows 404 page
  }

  return (
    <div>
      <h1>{profile.full_name}</h1>
      <p>{profile.bio}</p>
    </div>
  );
}

Routes already structured

Delfy uses dynamic routes for blog posts, docs, profiles, and more — all following Next.js 15 patterns with proper params handling and metadata.

See what's included
Dynamic routes in Next.js 15 — params, catch-all, and examples | Delfy.dev Blog