Skip to content

Next.js Dynamic Routing

Dynamic routing is one of the core features of Next.js, allowing you to create pages based on parameters. This chapter will detail how to use dynamic routing to build flexible applications.

Dynamic Routing Basics

App Router Dynamic Routes

In App Router, use square brackets [] to create dynamic routes:

app/
├── blog/
│   ├── page.tsx          # /blog
│   └── [slug]/
│       └── page.tsx      # /blog/[slug]
└── users/
    └── [id]/
        └── page.tsx      # /users/[id]

Pages Router Dynamic Routes

In Pages Router:

pages/
├── blog/
│   ├── index.tsx         # /blog
│   └── [slug].tsx        # /blog/[slug]
└── users/
    └── [id].tsx          # /users/[id]

Single Dynamic Route

App Router Implementation

typescript
// app/blog/[slug]/page.tsx
interface PageProps {
  params: {
    slug: string
  }
}

export default function BlogPost({ params }: PageProps) {
  return (
    <div>
      <h1>Blog Post</h1>
      <p>Post slug: {params.slug}</p>
    </div>
  )
}

// Generate static params (optional)
export async function generateStaticParams() {
  const posts = await fetchPosts()
  
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

Pages Router Implementation

typescript
// pages/blog/[slug].tsx
import { useRouter } from 'next/router'
import { GetStaticProps, GetStaticPaths } from 'next'

interface BlogPostProps {
  post: {
    title: string
    content: string
    slug: string
  }
}

export default function BlogPost({ post }: BlogPostProps) {
  const router = useRouter()
  
  // Show loading state if page hasn't been generated yet
  if (router.isFallback) {
    return <div>Loading...</div>
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await fetchPosts()
  
  const paths = posts.map((post) => ({
    params: { slug: post.slug }
  }))
  
  return {
    paths,
    fallback: false // or true or 'blocking'
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const post = await fetchPost(params?.slug as string)
  
  if (!post) {
    return {
      notFound: true,
    }
  }
  
  return {
    props: { post },
    revalidate: 3600, // ISR - regenerate every hour
  }
}

Multi-level Dynamic Routes

Nested Dynamic Routes

typescript
// app/blog/[category]/[slug]/page.tsx
interface PageProps {
  params: {
    category: string
    slug: string
  }
}

export default function CategoryPost({ params }: PageProps) {
  return (
    <div>
      <h1>Category Post</h1>
      <p>Category: {params.category}</p>
      <p>Post: {params.slug}</p>
    </div>
  )
}

export async function generateStaticParams() {
  const posts = await fetchAllPosts()
  
  return posts.map((post) => ({
    category: post.category,
    slug: post.slug,
  }))
}

User Profile Page Example

typescript
// app/users/[id]/profile/page.tsx
interface PageProps {
  params: {
    id: string
  }
}

export default async function UserProfile({ params }: PageProps) {
  const user = await fetchUser(params.id)
  
  if (!user) {
    return <div>User not found</div>
  }
  
  return (
    <div>
      <h1>{user.name}'s Profile</h1>
      <p>Email: {user.email}</p>
      <p>Joined: {user.createdAt}</p>
    </div>
  )
}

Catch-all Routes

Using [...slug] Syntax

typescript
// app/docs/[...slug]/page.tsx
interface PageProps {
  params: {
    slug: string[]
  }
}

export default function DocsPage({ params }: PageProps) {
  const path = params.slug.join('/')
  
  return (
    <div>
      <h1>Documentation Page</h1>
      <p>Path: /{path}</p>
      <p>Segments: {JSON.stringify(params.slug)}</p>
    </div>
  )
}

// Matches:
// /docs/getting-started -> slug: ['getting-started']
// /docs/api/users -> slug: ['api', 'users']
// /docs/guide/advanced/hooks -> slug: ['guide', 'advanced', 'hooks']

Optional Catch-all Routes

typescript
// app/shop/[[...slug]]/page.tsx
interface PageProps {
  params: {
    slug?: string[]
  }
}

export default function ShopPage({ params }: PageProps) {
  if (!params.slug) {
    // Matches /shop
    return <div>Shop Homepage</div>
  }
  
  const [category, subcategory, product] = params.slug
  
  if (product) {
    // Matches /shop/electronics/phones/iphone
    return <div>Product Page: {product}</div>
  }
  
  if (subcategory) {
    // Matches /shop/electronics/phones
    return <div>Subcategory: {subcategory}</div>
  }
  
  // Matches /shop/electronics
  return <div>Category: {category}</div>
}

Query Parameters and Search Params

App Router Search Params Handling

typescript
// app/search/page.tsx
interface PageProps {
  searchParams: {
    q?: string
    page?: string
    category?: string
  }
}

export default function SearchPage({ searchParams }: PageProps) {
  const query = searchParams.q || ''
  const page = parseInt(searchParams.page || '1')
  const category = searchParams.category
  
  return (
    <div>
      <h1>Search Results</h1>
      <p>Query: {query}</p>
      <p>Page: {page}</p>
      {category && <p>Category: {category}</p>}
    </div>
  )
}

// URL: /search?q=nextjs&page=2&category=tutorial

Client-side Search Params

typescript
'use client'

import { useSearchParams } from 'next/navigation'

export default function SearchComponent() {
  const searchParams = useSearchParams()
  
  const query = searchParams.get('q')
  const page = searchParams.get('page')
  
  return (
    <div>
      <p>Query: {query}</p>
      <p>Page: {page}</p>
    </div>
  )
}

Route Parameter Validation

Parameter Type Validation

typescript
// app/users/[id]/page.tsx
import { notFound } from 'next/navigation'

interface PageProps {
  params: {
    id: string
  }
}

export default async function UserPage({ params }: PageProps) {
  // Validate ID format
  const userId = parseInt(params.id)
  
  if (isNaN(userId) || userId <= 0) {
    notFound()
  }
  
  const user = await fetchUser(userId)
  
  if (!user) {
    notFound()
  }
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>User ID: {userId}</p>
    </div>
  )
}

Using Zod Validation

typescript
// lib/validations.ts
import { z } from 'zod'

export const userParamsSchema = z.object({
  id: z.string().regex(/^\d+$/, 'User ID must be a number')
})

export const postParamsSchema = z.object({
  slug: z.string().min(1, 'slug cannot be empty').regex(/^[a-z0-9-]+$/, 'Invalid slug format')
})
typescript
// app/users/[id]/page.tsx
import { userParamsSchema } from '@/lib/validations'
import { notFound } from 'next/navigation'

export default async function UserPage({ params }: { params: { id: string } }) {
  try {
    const { id } = userParamsSchema.parse(params)
    const user = await fetchUser(parseInt(id))
    
    if (!user) {
      notFound()
    }
    
    return <div>{user.name}</div>
  } catch (error) {
    notFound()
  }
}

Dynamic Navigation

Programmatic Navigation

typescript
'use client'

import { useRouter } from 'next/navigation'

export default function NavigationExample() {
  const router = useRouter()
  
  const goToUser = (userId: number) => {
    router.push(`/users/${userId}`)
  }
  
  const goToPost = (slug: string) => {
    router.push(`/blog/${slug}`)
  }
  
  const goWithQuery = () => {
    router.push('/search?q=nextjs&page=1')
  }
  
  return (
    <div>
      <button onClick={() => goToUser(123)}>
        View User 123
      </button>
      <button onClick={() => goToPost('my-first-post')}>
        View Post
      </button>
      <button onClick={goWithQuery}>
        Search Next.js
      </button>
    </div>
  )
}
typescript
import Link from 'next/link'

interface Post {
  id: number
  slug: string
  title: string
}

interface PostListProps {
  posts: Post[]
}

export default function PostList({ posts }: PostListProps) {
  return (
    <div>
      {posts.map((post) => (
        <Link
          key={post.id}
          href={`/blog/${post.slug}`}
          className="block p-4 border rounded hover:bg-gray-50"
        >
          <h3>{post.title}</h3>
        </Link>
      ))}
    </div>
  )
}

SEO Optimization for Dynamic Routes

Dynamic Metadata

typescript
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

interface PageProps {
  params: { slug: string }
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const post = await fetchPost(params.slug)
  
  if (!post) {
    return {
      title: 'Post Not Found',
    }
  }
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default async function BlogPost({ params }: PageProps) {
  const post = await fetchPost(params.slug)
  
  if (!post) {
    return <div>Post not found</div>
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Structured Data

typescript
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProduct(params.id)
  
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.images,
    offers: {
      '@type': 'Offer',
      price: product.price,
      priceCurrency: 'USD',
      availability: 'https://schema.org/InStock',
    },
  }
  
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <div>
        <h1>{product.name}</h1>
        <p>{product.description}</p>
        <p>Price: ${product.price}</p>
      </div>
    </>
  )
}

Performance Optimization

Prefetching and Preloading

typescript
'use client'

import Link from 'next/link'
import { useRouter } from 'next/navigation'

export default function PostCard({ post }: { post: Post }) {
  const router = useRouter()
  
  return (
    <div
      onMouseEnter={() => {
        // Prefetch page on hover
        router.prefetch(`/blog/${post.slug}`)
      }}
    >
      <Link href={`/blog/${post.slug}`}>
        <h3>{post.title}</h3>
        <p>{post.excerpt}</p>
      </Link>
    </div>
  )
}

Incremental Static Regeneration (ISR)

typescript
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Revalidate every hour

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetchPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Error Handling

Custom 404 Page

typescript
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
  return (
    <div className="text-center py-20">
      <h1 className="text-4xl font-bold mb-4">Post Not Found</h1>
      <p className="text-gray-600 mb-8">
        Sorry, the post you're looking for doesn't exist or has been deleted.
      </p>
      <Link
        href="/blog"
        className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
      >
        Back to Blog
      </Link>
    </div>
  )
}

Error Boundary

typescript
// app/blog/[slug]/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="text-center py-20">
      <h1 className="text-4xl font-bold mb-4">Error</h1>
      <p className="text-gray-600 mb-8">
        An error occurred while loading the post: {error.message}
      </p>
      <button
        onClick={reset}
        className="bg-red-500 text-white px-6 py-2 rounded hover:bg-red-600"
      >
        Try Again
      </button>
    </div>
  )
}

Practical Application Example

E-commerce Product Page

typescript
// app/products/[category]/[id]/page.tsx
interface PageProps {
  params: {
    category: string
    id: string
  }
}

export async function generateStaticParams() {
  const products = await fetchAllProducts()
  
  return products.map((product) => ({
    category: product.category,
    id: product.id.toString(),
  }))
}

export async function generateMetadata({ params }: PageProps) {
  const product = await fetchProduct(params.id)
  
  return {
    title: `${product.name} - ${product.category}`,
    description: product.description,
  }
}

export default async function ProductPage({ params }: PageProps) {
  const product = await fetchProduct(params.id)
  
  return (
    <div className="max-w-4xl mx-auto p-6">
      <nav className="mb-6">
        <Link href="/products">Products</Link>
        {' > '}
        <Link href={`/products/${params.category}`}>
          {params.category}
        </Link>
        {' > '}
        <span>{product.name}</span>
      </nav>
      
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
        <div>
          <img
            src={product.image}
            alt={product.name}
            className="w-full rounded-lg"
          />
        </div>
        <div>
          <h1 className="text-3xl font-bold mb-4">{product.name}</h1>
          <p className="text-2xl text-green-600 mb-4">${product.price}</p>
          <p className="text-gray-600 mb-6">{product.description}</p>
          <button className="bg-blue-500 text-white px-8 py-3 rounded-lg hover:bg-blue-600">
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  )
}

Summary

Next.js dynamic routing provides powerful features:

  • Flexible route structure - Support for single and multiple dynamic segments
  • Catch-all routes - Handle paths of any depth
  • Type safety - TypeScript support for parameter types
  • SEO friendly - Dynamic metadata and structured data
  • Performance optimization - Prefetching, ISR, and other optimization strategies

By using dynamic routing effectively, you can build flexible, scalable applications while maintaining excellent user experience and SEO performance.

Content is for learning and research only.