Switch Language
Toggle Theme

Next.js Dynamic Routing & Parameter Handling Complete Guide: From Basics to Type Safety

Introduction

Last week while refactoring a Next.js project, I ran into a frustrating problem—I wrote the dynamic route exactly as the documentation said, but clicking into it gave me a 404. The console was completely silent, no error messages at all, and I was completely confused. Later I discovered that Next.js 14’s App Router changed how routing parameters are accessed, and I was still using the old Pages Router approach.

Honestly, this wasn’t my first stumble with Next.js routing. From Pages Router’s getStaticPaths to App Router’s generateStaticParams, every upgrade requires relearning. When should you use dynamic routes? When should you use catch-all routes? What about optional parameters? These concepts mixed together can really get confusing.

If you’re also confused about Next.js dynamic routing, or migrating from Pages Router to App Router, this article is for you. I’ll start from the most basic dynamic routing, all the way to type safety practice techniques, using extensive real code examples to help clarify things.

What will you get after learning? A complete dynamic routing knowledge system, knowing which route type to use in various scenarios, how to correctly access parameters, and how to use TypeScript to give routing parameters type hints. No fluff—just solid code and solutions. Let’s begin.

Chapter 1: Dynamic Routing Basics (Starting from the Simplest)

What Is Dynamic Routing?

Let’s start with the most common scenario: you have a blog website, each article’s URL is /blog/articleID. If you use static routes, you’d need to create a separate page file for each article, which is obviously impractical. This is where dynamic routing comes in—one page file handles all article details.

In Next.js App Router, dynamic routing is implemented through bracket-named folders. Sounds a bit convoluted, let’s see an example:

app/
├── blog/
│   └── [slug]/
│       └── page.tsx    ← This is a dynamic route

This structure matches all /blog/* paths, for example:

  • /blog/hello-worldslug = "hello-world"
  • /blog/nextjs-guideslug = "nextjs-guide"
  • /blog/123slug = "123"

Simplest Dynamic Route Implementation

Create app/blog/[slug]/page.tsx, write this code:

// app/blog/[slug]/page.tsx
export default function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  return (
    <div>
      <h1>Article Details</h1>
      <p>Current article slug: {params.slug}</p>
    </div>
  )
}

That’s it! When a user visits /blog/hello-world, params.slug will be "hello-world".

Common beginner mistakes:

  1. ❌ Using filename [slug].tsx (App Router needs folders)
  2. ❌ Accessing props.slug directly (must get through params object)
  3. ❌ Forgetting brackets in folder name (without brackets it’s a static route)

Pages Router vs App Router Comparison

If you’ve used Pages Router before, you might find it strange: “Didn’t we write in pages/blog/[slug].tsx before?” Yes, App Router changed quite a bit:

FeaturePages RouterApp Router
File Locationpages/blog/[slug].tsxapp/blog/[slug]/page.tsx
Parameter Accessrouter.query.slug or getStaticPropsparams.slug
Type DefinitionManual definition requiredInferred through props types
Static GenerationgetStaticPathsgenerateStaticParams

When I first migrated, the parameter access method was the hardest to get used to. Pages Router can use useRouter hook, but App Router’s Server Components can’t use hooks—only through params prop. This is because Server Components render on the server by default, there’s no client-side router object.

Practical Case: E-commerce Product Detail Page

Let’s say you’re building an e-commerce site, product detail page URL is /products/productID. Complete implementation looks like this:

// app/products/[id]/page.tsx
interface Product {
  id: string
  name: string
  price: number
  description: string
}

// Simulate fetching product from database
async function getProduct(id: string): Promise<Product | null> {
  // In real projects this would be a database query or API call
  const products: Product[] = [
    { id: '1', name: 'TypeScript入门书', price: 99, description: '适合初学者' },
    { id: '2', name: 'React 实战指南', price: 129, description: '从零到项目上线' }
  ]
  return products.find(p => p.id === id) || null
}

export default async function ProductPage({
  params
}: {
  params: { id: string }
}) {
  const product = await getProduct(params.id)

  if (!product) {
    return <div>Product not found</div>
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p className="price">¥{product.price}</p>
      <p>{product.description}</p>
    </div>
  )
}

Note these details:

  1. Component uses async because Server Components support async
  2. Fetch data first, then decide what to render based on results
  3. Handled product not found case (404 scenario)

If you want to return a real 404 page, use Next.js’s notFound function:

import { notFound } from 'next/navigation'

export default async function ProductPage({
  params
}: {
  params: { id: string }
}) {
  const product = await getProduct(params.id)

  if (!product) {
    notFound() // Return 404 page
  }

  return (
    <div>
      <h1>{product.name}</h1>
      {/* ... */}
    </div>
  )
}

This way, when users visit a non-existent product, they’ll see your custom not-found.tsx page—better user experience.

At this point, you’ve mastered basic dynamic routing. But this is just the tip of the iceberg. Next, let’s see more complex scenarios—what to do when you need to match multi-level paths.

Chapter 2: Catch-All Routes & Optional Parameters (Handling Complex Paths)

When Do You Need Catch-All Routes?

Let’s say you’re building a documentation site, URL structure is like this:

  • /docs/getting-started
  • /docs/api/authentication
  • /docs/api/database/queries
  • /docs/guides/deployment/vercel

Path levels aren’t fixed—could be 2 levels, could be 3 or more. Regular dynamic routes can’t handle this, so you need Catch-All routes.

Catch-All Route: [...slug]

Name the folder [...slug] (three dots), can match paths of any level:

app/
├── docs/
│   └── [...slug]/
│       └── page.tsx    ← Matches all paths under /docs/*

This will match:

  • /docs/getting-startedslug = ["getting-started"]
  • /docs/api/authenticationslug = ["api", "authentication"]
  • /docs/guides/deployment/vercelslug = ["guides", "deployment", "vercel"]

Note: The slug parameter is an array, not a string!

Code Implementation: Documentation System

// app/docs/[...slug]/page.tsx
interface Doc {
  title: string
  content: string
}

// Get document based on path array
async function getDoc(slugArray: string[]): Promise<Doc | null> {
  // Join array into path, e.g., ["api", "auth"] → "api/auth"
  const path = slugArray.join('/')

  // In real projects, read from file system or database
  const docs: Record<string, Doc> = {
    'getting-started': {
      title: '快速开始',
      content: '欢迎使用我们的产品...'
    },
    'api/authentication': {
      title: 'API 认证',
      content: '我们使用 JWT 进行认证...'
    },
    'api/database/queries': {
      title: '数据库查询',
      content: '使用 Prisma 查询数据库...'
    }
  }

  return docs[path] || null
}

export default async function DocsPage({
  params
}: {
  params: { slug: string[] }
}) {
  const doc = await getDoc(params.slug)

  if (!doc) {
    return <div>Document not found</div>
  }

  return (
    <article>
      <h1>{doc.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: doc.content }} />

      {/* Breadcrumb navigation */}
      <nav>
        <a href="/docs">Docs</a>
        {params.slug.map((segment, i) => {
          const href = `/docs/${params.slug.slice(0, i + 1).join('/')}`
          return (
            <span key={i}>
              {' / '}
              <a href={href}>{segment}</a>
            </span>
          )
        })}
      </nav>
    </article>
  )
}

Highlights of this code:

  1. Use slugArray.join('/') to join path array into string
  2. Implemented breadcrumb navigation using slice to extract path prefixes
  3. Type annotation params: { slug: string[] }, TypeScript will check you didn’t write it wrong

Optional Catch-All Route: [[...slug]]

Sometimes you want to match both /docs and /docs/*. Regular Catch-All routes won’t match /docs (no parameters), so use Optional Catch-All route:

app/
├── docs/
│   └── [[...slug]]/
│       └── page.tsx    ← Note the double brackets

This will match:

  • /docsslug = undefined
  • /docs/getting-startedslug = ["getting-started"]
  • /docs/api/authslug = ["api", "auth"]

In code, you need to handle slug possibly being undefined:

// app/docs/[[...slug]]/page.tsx
export default async function DocsPage({
  params
}: {
  params: { slug?: string[] }  // Note slug is optional
}) {
  // If it's /docs homepage
  if (!params.slug) {
    return <div>Welcome to Documentation Center</div>
  }

  // Handle sub-paths
  const doc = await getDoc(params.slug)
  // ...
}

Common Beginner Pitfalls

Pitfall 1: Forgetting slug is an array

// ❌ Wrong approach
<h1>Current path: {params.slug}</h1>  // Will display "api,authentication"

// ✅ Correct approach
<h1>Current path: {params.slug.join('/')}</h1>  // Displays "api/authentication"

Pitfall 2: Wrong data structure in static generation

// ❌ Wrong approach
export function generateStaticParams() {
  return [
    { slug: 'api/auth' }  // This is a string, not an array!
  ]
}

// ✅ Correct approach
export function generateStaticParams() {
  return [
    { slug: ['api', 'auth'] }  // Array form
  ]
}

Pitfall 3: Confusing regular dynamic routes and Catch-All routes

Route TypeFolder NamingMatch RangeParameter Type
Dynamic Route[slug]/blog/123string
Catch-All[...slug]/docs/a/b/c (excluding /docs)string[]
Optional Catch-All[[...slug]]/docs and /docs/a/b/cstring[] | undefined

I mixed these three together, routes worked sometimes and didn’t other times—took me a while to discover the folder naming was wrong.

Practical Tip: Handling Special Characters

If URLs contain Chinese or special characters, remember to encode/decode:

export default async function Page({
  params
}: {
  params: { slug: string[] }
}) {
  // URLs are automatically encoded, need to decode for normal display
  const decodedSlug = params.slug.map(s => decodeURIComponent(s))

  console.log(params.slug)      // ["api", "%E8%AE%A4%E8%AF%81"]
  console.log(decodedSlug)      // ["api", "认证"]

  // ...
}

At this point, you can handle various complex path structures. But there’s still a key question: when are these dynamic pages generated? Render on every request, or pre-generate at build time? This is what the next chapter covers—generateStaticParams.

Chapter 3: generateStaticParams Deep Dive (When to Use, How to Use)

Why Do You Need generateStaticParams?

Let’s say your blog has 100 articles, each article is a dynamic route /blog/[slug]. Without optimization, every user visit requires:

  1. Query database to get article content
  2. Server-side render HTML
  3. Return to user

This is slow and puts pressure on the server. Next.js provides a better solution—pre-render all article pages at build time, generating static HTML. This is what generateStaticParams does.

Basic Usage: Static Generation of Blog Articles

// app/blog/[slug]/page.tsx
interface Post {
  slug: string
  title: string
  content: string
}

// Get all article slugs
export async function generateStaticParams() {
  // Get all articles from database or CMS
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  // Return all possible parameter combinations
  return posts.map((post: Post) => ({
    slug: post.slug
  }))
}

// Render article details
export default async function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  // Get article content based on slug
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(r => r.json())

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

What does this code do?

  1. generateStaticParams runs at build time, returns all article slugs
  2. Next.js pre-renders a static HTML file for each slug
  3. When users visit, directly return static file—super fast

Build output:

.next/server/app/blog/
├── hello-world.html
├── nextjs-guide.html
└── typescript-tips.html

When to Use generateStaticParams?

This is the question I get asked most. Simple judgment:

Scenarios suitable for generateStaticParams:

  • Blog articles, news details (content relatively fixed)
  • Product detail pages (limited product count, e.g., < 10000)
  • Documentation pages, help centers
  • User profile pages (if user count isn’t huge)

Scenarios not suitable:

  • Search results pages (infinite parameter combinations)
  • Real-time data (stock quotes, sports scores)
  • Massive UGC platforms (can’t pre-render all user pages)
  • Pages that need to display different content based on login status

Advanced Usage 1: Static Generation for Catch-All Routes

For [...slug] routes, returned parameters must be arrays:

// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
  // All documentation paths
  const docPaths = [
    ['getting-started'],
    ['api', 'authentication'],
    ['api', 'database', 'queries'],
    ['guides', 'deployment', 'vercel']
  ]

  return docPaths.map(slug => ({ slug }))
}

export default async function DocsPage({
  params
}: {
  params: { slug: string[] }
}) {
  // ...
}

Note: Return format is { slug: ['api', 'auth'] }, not { slug: 'api/auth' }.

Advanced Usage 2: Multi-Parameter Routes

If a route has multiple dynamic parameters, like /shop/[category]/[productId]:

app/
├── shop/
│   └── [category]/
│       └── [productId]/
│           └── page.tsx

Write generateStaticParams like this:

// app/shop/[category]/[productId]/page.tsx
export async function generateStaticParams() {
  const products = [
    { category: 'electronics', productId: 'iphone-15' },
    { category: 'electronics', productId: 'macbook-pro' },
    { category: 'books', productId: 'clean-code' },
    { category: 'books', productId: 'refactoring' }
  ]

  return products.map(p => ({
    category: p.category,
    productId: p.productId
  }))
}

export default async function ProductPage({
  params
}: {
  params: { category: string; productId: string }
}) {
  return (
    <div>
      <h1>Category: {params.category}</h1>
      <p>Product ID: {params.productId}</p>
    </div>
  )
}

Advanced Usage 3: On-Demand Generation (Fallback Mode)

If you have too much content (like 100,000 articles), pre-rendering everything isn’t realistic. You can only generate popular content, generate the rest on-demand:

// app/blog/[slug]/page.tsx
export const dynamicParams = true  // Allow dynamic generation of non-pre-rendered pages

export async function generateStaticParams() {
  // Only pre-render top 100 popular articles
  const topPosts = await fetchTopPosts(100)

  return topPosts.map(post => ({
    slug: post.slug
  }))
}

export default async function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  // Even if not pre-rendered, first visit will generate page and cache it
  const post = await fetchPost(params.slug)

  if (!post) {
    notFound()
  }

  return <article>{/* ... */}</article>
}

After setting dynamicParams = true:

  • Pre-rendered pages: Return immediately (fastest)
  • Non-pre-rendered pages: Generate on first request, subsequent visits reuse cache
  • Non-existent pages: Return 404

Common Beginner Stumbling Blocks

Question 1: When does generateStaticParams run?

Only at build time (npm run build), not on every request. So you won’t see the effect in development (npm run dev), must build first to see statically generated files.

Question 2: What if data updates?

After static generation, content is fixed. If data updates, need to rebuild and redeploy. Solutions:

  • Use ISR (Incremental Static Regeneration) for scheduled updates
  • Combine with dynamicParams = true for on-demand updates
  • Use revalidate to set cache expiration time
// Regenerate page every 60 seconds
export const revalidate = 60

export default async function Page() {
  // ...
}

Question 3: Why did build time get longer?

The more paths generateStaticParams returns, the longer the build time. If build times out:

  • Reduce number of pre-rendered pages (only render popular content)
  • Use incremental builds (Vercel/Netlify support)
  • Consider on-demand generation (dynamicParams = true)

At this point, you’ve mastered Next.js dynamic routing core usage. Last chapter, we solve a problem that troubles many people—how to give routing parameters TypeScript type hints?

Chapter 4: Routing Parameter Type Safety Practices (Say Goodbye to any)

Why Do You Need Type Safety?

Look at this code, can you spot the problem?

export default async function Page({
  params
}: {
  params: { slug: string }
}) {
  // Assume this is a numeric ID, but type definition is string
  const id = parseInt(params.slug)

  if (isNaN(id)) {
    // Only discover type mismatch at runtime!
    return <div>Invalid ID</div>
  }

  // ...
}

The problem: params.slug type is string, but you actually need a number. This type mismatch can’t be caught at compile time, only fails at runtime.

Basic Type Constraints

Next.js’s params object defaults all parameters to string or string[]. You can customize types to enhance constraints:

// app/blog/[slug]/page.tsx
interface BlogParams {
  slug: string
}

export default async function BlogPost({
  params
}: {
  params: BlogParams
}) {
  // TypeScript knows params.slug is string
  const post = await fetchPost(params.slug)
  // ...
}

This doesn’t seem very useful, but when you have more parameters it becomes valuable:

// app/shop/[category]/[productId]/page.tsx
interface ShopParams {
  category: 'electronics' | 'books' | 'clothing'  // Limited to specific values
  productId: string
}

export default async function ProductPage({
  params
}: {
  params: ShopParams
}) {
  // TypeScript will check if category is an allowed value
  if (params.category === 'toys') {  // ❌ Compile error!
    // ...
  }
}

Runtime Validation: Combining with Zod

Type definitions only check at compile time; runtime can still receive illegal values. Combining with Zod for runtime validation is safer:

npm install zod
// app/products/[id]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'

// Define parameter schema
const paramsSchema = z.object({
  id: z.string().regex(/^\d+$/, 'Must be numeric ID')
})

export default async function ProductPage({
  params
}: {
  params: { id: string }
}) {
  // Runtime validation
  const result = paramsSchema.safeParse(params)

  if (!result.success) {
    notFound()  // Illegal parameters directly return 404
  }

  const { id } = result.data
  const product = await fetchProduct(parseInt(id))
  // ...
}

Benefits of this approach:

  • Type checking at compile time
  • Runtime validation of parameter format
  • Illegal requests directly return 404, won’t query database

Advanced Technique: Type-Safe generateStaticParams

generateStaticParams return value can also have type constraints:

// app/blog/[slug]/page.tsx
interface BlogParams {
  slug: string
}

export async function generateStaticParams(): Promise<BlogParams[]> {
  const posts = await fetchAllPosts()

  return posts.map(post => ({
    slug: post.slug
    // If you write slug: post.id (wrong type), TypeScript will error
  }))
}

export default async function BlogPost({
  params
}: {
  params: BlogParams
}) {
  // ...
}

Practical Case: Multi-Language Blog Routing

Let’s say you’re building a multi-language blog, URL is /[locale]/blog/[slug], for example:

  • /zh/blog/hello-world
  • /en/blog/hello-world

Complete type-safe implementation:

// app/[locale]/blog/[slug]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'

// Supported language list
const locales = ['zh', 'en', 'ja'] as const
type Locale = typeof locales[number]  // "zh" | "en" | "ja"

interface PageParams {
  locale: Locale
  slug: string
}

// Runtime validation schema
const paramsSchema = z.object({
  locale: z.enum(locales),
  slug: z.string().min(1)
})

export async function generateStaticParams(): Promise<PageParams[]> {
  const posts = await fetchAllPosts()

  // Generate corresponding paths for each language
  return locales.flatMap(locale =>
    posts.map(post => ({
      locale,
      slug: post.slug
    }))
  )
}

export default async function BlogPost({
  params
}: {
  params: PageParams
}) {
  // Runtime validation
  const result = paramsSchema.safeParse(params)
  if (!result.success) {
    notFound()
  }

  const { locale, slug } = result.data

  // Get article in corresponding language
  const post = await fetchPost(slug, locale)

  if (!post) {
    notFound()
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

Advantages of this code:

  1. Locale type limited to "zh" | "en" | "ja", errors if you write wrong
  2. generateStaticParams return type is PageParams[], ensures correct structure
  3. Runtime Zod validation prevents illegal requests
  4. Entire flow from type definition to runtime validation is strict

Common Type Issue Troubleshooting

Issue 1: params type is Promise<...> what to do?

After Next.js 15, params might be async. Need to write like this:

export default async function Page({
  params
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params  // Await first
  // ...
}

Or use sync version (if you’re sure it’s Next.js 14):

export default async function Page({
  params
}: {
  params: { slug: string }
}) {
  // Use directly
}

Issue 2: Type hints not accurate

If TypeScript shows params as any, check:

  1. Is strict mode enabled in tsconfig.json?
  2. Are Next.js types correctly imported?
  3. Is file naming correct? (Must be page.tsx)

Issue 3: Zod validation fails but want to see detailed errors

const result = paramsSchema.safeParse(params)

if (!result.success) {
  console.error('Parameter validation failed:', result.error.format())
  notFound()
}

Type Safety Checklist

In your project, check these points to ensure type safety:

  • All dynamic route pages have params type defined
  • generateStaticParams return type matches params
  • Critical routes use runtime validation (Zod)
  • TypeScript strict mode enabled
  • Complex parameters use union types or literal types

Do these, and your routing system will basically avoid type-related bugs.

Conclusion

If you’ve followed this article to this point, congratulations! You now have a complete Next.js dynamic routing knowledge system. Let’s review what you’ve learned:

Basic Dynamic Routing: Use [slug] to match single-level paths, understand params parameter access
Catch-All Routes: Use [...slug] to handle multi-level paths, know optional parameter usage
generateStaticParams: Understand when to use, how to use, and on-demand generation strategies
Type Safety Practices: Complete solution from compile-time type constraints to runtime validation

More importantly, you understand the differences between App Router and Pages Router, and won’t confuse their usage anymore. You also know when to pre-render, when to generate on-demand, and can choose appropriate solutions based on actual scenarios.

What Can You Do Next?

Immediate Practice (Don’t procrastinate):

  • Create a dynamic route in your project, try params parameter access
  • If you have multi-level path needs, try Catch-All routes
  • Add TypeScript type definitions and Zod validation to your routes

Advanced Learning (Deep mastery):

  • Parallel Routes: Load multiple routes on the same page (@folder syntax)
  • Intercepting Routes: Display another route without leaving current page ((.)folder syntax)
  • Route Groups: Organize routes with (folder) without affecting URL structure
  • Middleware: Permission control and redirects at route level

Learning Resources (Official is most authoritative):

Common Issues Quick Reference:

IssueCheck ItemsSolution
Dynamic route 404Folder naming, generateStaticParamsConfirm brackets correct, check static generation config
params is anyTypeScript configEnable strict mode, define parameter types
Build time too longgenerateStaticParams return countReduce pre-rendered pages, use on-demand generation
Data not updatingCache strategySet revalidate or dynamicParams

Final Thoughts

Next.js routing system has changed significantly from Pages Router to App Router, and I know many people (including myself) have experienced migration pains. But once you master App Router’s way of thinking, you’ll find it’s actually more intuitive and powerful.

Dynamic routing is just one aspect of Next.js, but it’s the foundation of the entire application. Once you understand routing, concepts like data fetching, cache strategies, and middleware will come much more smoothly.

If you encounter problems in practice:

  1. First check the official docs’ “Troubleshooting” section
  2. Search related Issues in Next.js GitHub repository
  3. Ask in Next.js Discord community (English, but responds quickly)

Don’t be afraid to make mistakes—I also struggled through several projects before fully understanding App Router’s routing mechanism. Now you have this article as a reference, should save you a lot of detours.

Now, open your editor and start building your dynamic routes! 🚀

FAQ

How do I get dynamic route parameters?
App Router uses params object to get parameters.

Key points:
• params is a Promise, must await
• Use destructuring to get specific parameters
• Types need to be defined

Example:
export default async function Page({ params }) {
const { id } = await params
return <div>{id}</div>
}
Why does dynamic route return 404?
Possible causes:
• Folder naming error (should be [id] not {id})
• Path mismatch (check URL and folder structure)
• generateStaticParams returns incomplete data
• Missing page.tsx file

Solutions:
• Check if folder naming is correct
• Confirm URL path matches folder structure
• Check generateStaticParams return value
What's the difference between catch-all and optional catch-all routes?
Catch-all route [...slug]:
• Must match at least one path segment
• /posts/[...slug] matches /posts/a, but not /posts

Optional catch-all route [[...slug]]:
• Can match 0 or more path segments
• /posts/[[...slug]] matches /posts and /posts/a/b

Use cases:
• catch-all: need at least one parameter
• optional catch-all: parameters optional
How do I implement type-safe dynamic routes?
Steps:
1) Define params type interface
2) Use Promise<{ params }> type
3) Use generateStaticParams return type

Example:
interface PageProps {
params: Promise<{ id: string }>
}

export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
}
When should I use generateStaticParams?
For statically generating all possible pages.

Suitable scenarios:
• Know all possible parameter values
• Need to statically generate all pages
• Improve performance and SEO

Not suitable:
• Parameter values change dynamically
• Too many parameter values to enumerate
• Need real-time data

Note: Only for static generation, dynamic routes don't need it
How do I migrate dynamic routes from Pages Router?
Main changes:
• getStaticPaths → generateStaticParams
• context.params → params (need await)
• Return format changes from { paths, fallback } to array

Migration steps:
1) Change getStaticPaths to generateStaticParams
2) Modify parameter access (use await params)
3) Update type definitions
4) Test all routes
How do I handle multi-parameter dynamic routes?
Create multi-level folders:
app/posts/[category]/[id]/page.tsx

Get parameters:
export default async function Page({ params }) {
const { category, id } = await params
return <div>{category} - {id}</div>
}

generateStaticParams returns all combinations:
export async function generateStaticParams() {
return [
{ category: 'tech', id: '1' },
{ category: 'tech', id: '2' },
// ...
]
}

11 min read · Published on: Dec 25, 2025 · Modified on: Jan 22, 2026

Comments

Sign in with GitHub to leave a comment

Related Posts