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=tutorialClient-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>
)
}Link Component Navigation
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.