NextjsDynamic Routes

Dynamic Routes

Most real applications need routes whose path depends on data — a blog post slug, a product ID, a username. Next.js handles this with dynamic route segments: wrap a folder name in square brackets, like [id], and that segment becomes a variable the page can read at request time instead of a fixed string.

Folder structure

Text
app/
  blog/
    [slug]/
      page.tsx
Visiting /blog/hello-world or /blog/nextjs-tips both match this route, and in each case Next.js supplies the matched segment value through the page's params prop.

app/blog/[slug]/page.tsx

TSX
type Props = {
  params: { slug: string }
}

export default function BlogPostPage({ params }: Props) {
  const { slug } = params

  return <h1>Post: {slug}</h1>
}
Multiple dynamic segments
A route can contain more than one dynamic segment. Each bracketed folder becomes its own key on the params object.

Folder structure

Text
app/
  shop/
    [category]/
      [productId]/
        page.tsx

app/shop/[category]/[productId]/page.tsx

TSX
type Props = {
  params: { category: string; productId: string }
}

export default function ProductPage({ params }: Props) {
  const { category, productId } = params

  return (
    <p>
      Category: {category}, Product: {productId}
    </p>
  )
}
Note
Visiting /shop/shoes/42 gives you { category: "shoes", productId: "42" }.
Params are always strings
Regardless of what the segment "looks like" in the URL, Next.js always hands you a string (or, for catch-all routes, an array of strings). Even if a segment is conceptually a numeric ID, you must parse it yourself before using it as a number.

Parsing a numeric param

TSX
export default function ProductPage({
  params,
}: {
  params: { productId: string }
}) {
  const productId = Number(params.productId)

  if (Number.isNaN(productId)) {
    // handle invalid input, e.g. call notFound()
  }

  return <p>Product #{productId}</p>
}
Warning
Don't assume a param is a valid number, UUID, or slug just because your app only ever links to valid values. Users can type any URL directly, so validate and parse before trusting it.
Next.js 15: params became a Promise
This is a genuinely important, breaking-ish change. In Next.js 13 and 14, params (and searchParams) were plain synchronous objects. Starting with Next.js 15, in Server Components params is a Promise that you must await before reading its properties — part of a broader move to let Next.js start rendering before all route params are fully resolved.

Before — Next.js 13/14

TSX
export default function Page({
  params,
}: {
  params: { slug: string }
}) {
  return <h1>{params.slug}</h1>
}

After — Next.js 15+

TSX
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params

  return <h1>{slug}</h1>
}
Note
The same change applies to searchParams on the page component, and to the second argument of generateMetadata. If you're on Next.js 15+ and see a type error or a runtime warning about accessing a param synchronously, this is almost always the cause — check your Next.js version before copying examples from older tutorials or documentation.
  • A folder named [name] creates a dynamic segment matched against any value in that URL position.

  • The matched value is available on the params prop, keyed by the folder name.

  • A route can have multiple dynamic segments, each contributing its own key to params.

  • params values are always strings — parse them yourself if you need a number, boolean, etc.

  • Next.js 15+ makes params (and searchParams) a Promise in Server Components — you must await it.