Next.js Dynamic Routes

Create dynamic pages with brackets, access route params, and generate static paths with generateStaticParams.

TL;DR

  1. 01Wrap folder names in brackets to create dynamic segments.
  2. 02Access params through the params prop passed to pages.
  3. 03Use generateStaticParams to pre-build dynamic pages at compile time.

Tips

  1. 01Use <code>generateStaticParams()</code> for high-traffic pages like blog posts to pre-build them at deploy time for instant page loads.
  2. 02Combine <code>generateStaticParams()</code> with a <code>revalidate</code> export so high-traffic dynamic pages stay pre-built while still picking up fresh content automatically.

Warnings

  1. 01If generateStaticParams doesn't include a route, it will be generated on first request which causes a slow cold start on serverless.
  2. 02The object keys returned by <code>generateStaticParams()</code> must exactly match the dynamic segment names in the folder, or Next.js throws a build error: "generateStaticParams returned invalid params."

Basic Dynamic Routes

  • One folder name decides whether a route is static or matches infinite URLs — wrap it in square brackets to make it dynamic.
    app/
      posts/
        [id]/
          page.tsx
  • This matches /posts/1, /posts/2, or any value for the id segment.
  • Access the dynamic parameter through the params prop, which is now an async Promise you must await.
    export default async function Post({ params }: { params: Promise<{ id: string }> }) {
      const { id } = await params;
      return <h1>Post {id}</h1>;
    }
  • Each dynamic segment creates a separate route that can load different data.
  • URL parameters are always strings, so parse them as needed.

Accessing Route Parameters

  • Nest two dynamic segments and the params object resolves both at once, once you await it.
    // Route: app/users/[userId]/posts/[postId]/page.tsx
    export default async function PostPage({ 
      params 
    }: { 
      params: Promise<{ userId: string; postId: string }> 
    }) {
      const { userId, postId } = await params;
      return <div>User {userId}, Post {postId}</div>;
    }
  • Fetch data using the resolved params to load content specific to that route.
    const { id } = await params;
    const post = await getPost(id);
  • Pass params to layout components if they need this information.
  • Nested dynamic segments work just like single-level ones.

Catch-All Routes

  • Need one file to match /docs/a, /docs/a/b, and /docs/a/b/c alike? Catch-all segments do exactly that with [...slug].
    app/
      docs/
        [...slug]/
          page.tsx
  • This matches /docs/a, /docs/a/b, /docs/a/b/c, etc.
  • The slug param is always an array of path segments, available after awaiting params.
    export default async function Docs({ params }: { params: Promise<{ slug: string[] }> }) {
      const { slug } = await params;
      const path = slug.join("/");
      return <h1>{path}</h1>;
    }
  • Use catch-all routes for nested documentation or content sites.
  • Combine with optional catch-all [[...slug]] for breadcrumb navigation.

Optional Catch-All Routes

  • What if one page needs to handle both /blog and /blog/2025/january/post? Double the brackets to [[...slug]] and the catch-all becomes optional.
    app/
      blog/
        [[...slug]]/
          page.tsx
  • This matches /blog, /blog/post-1, /blog/2025/january/post, etc.
  • The slug param is an array only if segments exist, otherwise undefined — await params first to read it.
    const { slug } = await params;
    const segments = slug ?? [];
    const depth = segments.length;
  • Use optional catch-all when a single page handles multiple URL patterns.
  • Great for flexible navigation menus and hierarchical content.

Static Generation with Dynamic Routes

  • Skip the per-request server round trip entirely — export generateStaticParams() to pre-build specific dynamic pages at deploy time.
    export async function generateStaticParams() {
      const posts = await getPosts();
      return posts.map((post) => ({
        id: post.id.toString(),
      }));
    }
    
    export default async function Post({ params }: { params: Promise<{ id: string }> }) {
      const { id } = await params;
      // This page is pre-built for every post at build time
      return <h1>Post {id}</h1>;
    }
  • Pre-built pages load instantly with no server delay.
  • Pages not in generateStaticParams are generated on-demand the first time.
  • Use revalidate for incremental static regeneration to update stale pages.
    export const revalidate = 3600; // Rebuild every hour
  • Great for blogs, product pages, and public documentation.

FAQ