Most real-world applications need routes that work for any identifier—blog posts, user profiles, product pages, and more. You can't create a separate folder for every possible ID! That's where dynamic routes come in. With just square brackets around a folder name, Next.js creates routes that match any value, giving you incredible flexibility with minimal code.
Why We Need Dynamic Routes
Imagine building a blog with 1,000 posts. Without dynamic routes, you'd need:
app/
blog/
my-first-post/
page.tsx
hello-world/
page.tsx
nextjs-tutorial/
page.tsx
... 997 more folders!This is impossible to maintain! Instead, with dynamic routes, you create one folder that handles all blog posts:
app/
blog/
[slug]/ ← One folder handles all posts!
page.tsxNow this single route handles:
/blog/my-first-post/blog/hello-world/blog/nextjs-tutorial- ...and any other slug you can imagine!
The Power of Square Brackets
Wrapping a folder name in square brackets [name] tells Next.js: "This segment can match any value." Whatever appears in the URL at this position becomes available to your component via the params prop.
Creating Your First Dynamic Route
Let's create a blog with dynamic post routes step by step:
Step 1: Create the Folder Structure
app/
blog/
page.tsx ← Blog listing
[slug]/ ← Dynamic route folder
page.tsx ← Individual post pageStep 2: Create the Blog Listing Page
import Link from 'next/link';
const posts = [
{ slug: 'first-post', title: 'My First Post' },
{ slug: 'hello-world', title: 'Hello World' },
{ slug: 'learning-nextjs', title: 'Learning Next.js' },
];
export default function BlogPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Blog Posts</h1>
<div className="space-y-4">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="block p-4 border rounded-lg hover:shadow-lg transition"
>
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-blue-600">Read more →</p>
</Link>
))}
</div>
</div>
);
}Step 3: Create the Dynamic Post Page
// The params prop contains the dynamic segment
interface PageProps {
params: {
slug: string; // This matches the folder name [slug]
};
}
export default function BlogPostPage({ params }: PageProps) {
// params.slug contains whatever is in the URL
// /blog/first-post → params.slug = "first-post"
// /blog/hello-world → params.slug = "hello-world"
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-4">
Blog Post: {params.slug}
</h1>
<p className="text-gray-600">
This is the content for {params.slug}
</p>
</div>
);
}Step 4: Test It Out
Now visit these URLs in your browser:
http://localhost:3000/blog- See the listinghttp://localhost:3000/blog/first-post- See the post pagehttp://localhost:3000/blog/anything-you-want- Still works!
✨ It Just Works!
Notice how /blog/anything-you-want works even though we never explicitly created that route. That's the magic of dynamic routes!
How Dynamic Routes Work
Dynamic Route Examples
See how [brackets] create flexible route segments
📁 File Structure
app/
blog/
[slug]/
page.tsx🌐 URL Path
/blog/hello-world[slug] matches 'hello-world'. Access via params.slug
When a user visits a URL:
- Next.js looks for matching routes in the
appdirectory - If it finds a dynamic segment
[name], it captures that part of the URL - The captured value is passed to your page component as
params.name - Your component renders using that value
User visits: /blog/hello-world
Next.js matches: app/blog/[slug]/page.tsx
Extracts: slug = "hello-world"
Passes to component: params = { slug: "hello-world" }
Component receives: { params: { slug: "hello-world" } }Fetching Data with Dynamic Routes
In real applications, you'll use the dynamic parameter to fetch data from an API or database:
import { notFound } from 'next/navigation';
interface Post {
slug: string;
title: string;
content: string;
author: string;
date: string;
}
interface PageProps {
params: {
slug: string;
};
}
export default async function BlogPostPage({ params }: PageProps) {
// Fetch post data using the slug from the URL
const res = await fetch(
`https://api.example.com/posts/${params.slug}`
);
// Handle not found
if (!res.ok) {
notFound(); // Shows 404 page
}
const post: Post = await res.json();
return (
<article className="container mx-auto px-4 py-8 max-w-3xl">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-gray-600 mb-8">
<span>By {post.author}</span>
<span>•</span>
<time>{new Date(post.date).toLocaleDateString()}</time>
</div>
<div className="prose max-w-none">
{post.content}
</div>
</article>
);
}The notFound() Function
Import notFound from next/navigation and call it when a resource doesn't exist. This will display your not-found.tsx page or Next.js's default 404 page.
Multiple Dynamic Segments
You can have multiple dynamic segments in a single route:
Example: User Posts
app/
users/
[userId]/
page.tsx → /users/123
posts/
[postId]/
page.tsx → /users/123/posts/456interface PageProps {
params: {
userId: string; // First dynamic segment
postId: string; // Second dynamic segment
};
}
export default async function UserPostPage({ params }: PageProps) {
// Both params are available!
const user = await fetch(`/api/users/${params.userId}`)
.then(r => r.json());
const post = await fetch(`/api/users/${params.userId}/posts/${params.postId}`)
.then(r => r.json());
return (
<div>
<h1>{post.title}</h1>
<p>By {user.name}</p>
<div>{post.content}</div>
</div>
);
}Example: E-commerce Categories
app/
shop/
[category]/
page.tsx → /shop/electronics
[subcategory]/
page.tsx → /shop/electronics/laptopsinterface PageProps {
params: {
category: string;
subcategory: string;
};
}
export default async function SubcategoryPage({ params }: PageProps) {
const products = await fetch(
`/api/products?category=${params.category}&subcategory=${params.subcategory}`
).then(r => r.json());
return (
<div>
<h1>
{params.category} → {params.subcategory}
</h1>
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
</div>
);
}Dynamic Routes Structure
Explore different dynamic route patterns
Select a file or folder to see details
Naming Dynamic Segments
You can name dynamic segments whatever makes sense for your application:
Common Names
[id]- Numeric IDs[slug]- URL-friendly strings[username]- User identifiers[productId]- Product IDs[postId]- Post IDs[category]- Category names
Usage Examples
app/
posts/[id]/page.tsx
→ params.id
app/
blog/[slug]/page.tsx
→ params.slug
app/
users/[username]/page.tsx
→ params.username📝 Naming Best Practices
- Use descriptive names that indicate what the segment represents
- Be consistent across your app (always use 'id' or always use 'slug')
- Use camelCase for multi-word names: [userId], [postId]
- Match your database field names when possible
Generating Static Paths (Optional)
For dynamic routes that you want to pre-render at build time, use generateStaticParams:
// Tell Next.js which dynamic routes to pre-generate
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
// Return array of params objects
return posts.map((post) => ({
slug: post.slug,
}));
}
// This page will be pre-rendered for all returned slugs
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}This tells Next.js:
- "Pre-render these specific slugs at build time" (Static Site Generation)
- Other slugs can still be accessed (rendered on-demand)
- Improves performance for known, popular pages
When to Use generateStaticParams
- Blog posts that change rarely
- Product pages for your catalog
- Documentation pages
- Any content you want maximum performance for
Dynamic Metadata for SEO
Generate metadata based on the dynamic route parameter:
import { Metadata } from 'next';
interface PageProps {
params: { id: string };
}
// Generate metadata dynamically
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const product = await fetch(`https://api.example.com/products/${params.id}`)
.then(res => res.json());
return {
title: `${product.name} - Our Store`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
},
};
}
export default async function ProductPage({ params }: PageProps) {
const product = await fetch(`https://api.example.com/products/${params.id}`)
.then(res => res.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</div>
);
}This ensures each product page has unique, SEO-optimized metadata based on the actual product data!
Common Dynamic Route Patterns
1. Blog/Article Platform
app/
blog/
page.tsx → /blog (listing)
[slug]/
page.tsx → /blog/my-post
category/
[categorySlug]/
page.tsx → /blog/category/tutorials2. E-commerce Site
app/
products/
page.tsx → /products (all products)
[id]/
page.tsx → /products/123
reviews/
page.tsx → /products/123/reviews
categories/
[category]/
page.tsx → /categories/electronics3. Social Media Profile
app/
[username]/
page.tsx → /john (profile)
posts/
page.tsx → /john/posts
[postId]/
page.tsx → /john/posts/456
followers/
page.tsx → /john/followers4. Documentation Site
app/
docs/
page.tsx → /docs (intro)
[category]/
page.tsx → /docs/getting-started
[page]/
page.tsx → /docs/getting-started/installationLinking to Dynamic Routes
Use the Next.js Link component with template literals:
import Link from 'next/link';
function BlogList({ posts }) {
return (
<div>
{posts.map((post) => (
<Link
key={post.id}
href={`/blog/${post.slug}`}
>
{post.title}
</Link>
))}
</div>
);
}
// With multiple dynamic segments
function UserPostsList({ userId, posts }) {
return (
<div>
{posts.map((post) => (
<Link
key={post.id}
href={`/users/${userId}/posts/${post.id}`}
>
{post.title}
</Link>
))}
</div>
);
}Programmatic Navigation
"use client";
import { useRouter } from 'next/navigation';
export default function ProductCard({ productId }) {
const router = useRouter();
const handleClick = () => {
// Navigate to dynamic route programmatically
router.push(`/products/${productId}`);
};
return (
<button onClick={handleClick}>
View Product
</button>
);
}Practice: Build a Product Catalog
Let's build a complete product catalog with dynamic routes:
Product Detail Page with Dynamic Routes
Try changing the product ID in the URL (1, 2, or 3)
Output Preview
🎯 Try This Exercise
Create these dynamic routes in your project:
- A team member profile page:
app/team/[memberId]/page.tsx - A project showcase:
app/projects/[projectSlug]/page.tsx - Nested services:
app/services/[category]/[serviceId]/page.tsx
Dynamic Routes Best Practices
1. Validate Route Parameters
export default async function ProductPage({
params
}: { params: { id: string } }) {
// Validate the parameter
const id = parseInt(params.id);
if (isNaN(id) || id < 1) {
notFound();
}
// Continue with valid ID...
}2. Handle Not Found Cases
import { notFound } from 'next/navigation';
export default async function Page({ params }) {
const data = await fetch(`/api/items/${params.id}`)
.then(res => res.ok ? res.json() : null);
if (!data) {
notFound(); // Shows 404
}
return <div>{/* Render data */}</div>;
}3. Use TypeScript for Type Safety
// Define your params interface
interface PageProps {
params: {
id: string;
// Add all your dynamic segments
};
searchParams?: {
[key: string]: string | string[] | undefined;
};
}
export default async function Page({
params,
searchParams
}: PageProps) {
// TypeScript will catch errors!
}4. Sanitize User Input
export default async function Page({ params }) {
// Sanitize the parameter before using in queries
const safeSlug = params.slug
.toLowerCase()
.replace(/[^a-z0-9-]/g, '');
// Use sanitized value
const data = await fetchPost(safeSlug);
}Common Issues and Solutions
Issue 1: params is undefined
Problem: Getting "Cannot read property 'slug' of undefined"
Solutions:
- Make sure folder name matches: [slug] not [Slug] or [SLUG]
- Verify you're accessing the correct property name
- Check TypeScript interface matches folder name
Issue 2: Route returns 404
Problem: Dynamic route shows 404
Solutions:
- Ensure the folder has
page.tsxinside - Check folder name has square brackets: [id] not (id) or {id}
- Restart dev server after creating new dynamic routes
Issue 3: Getting wrong parameter value
Problem: params.id shows undefined but params.slug works
Solution: The property name in params matches the folder name. If your folder is [slug], use params.slug, not params.id
Key Takeaways
- Square brackets create dynamic routes - [id], [slug], [name]
- Dynamic segments match any single value at that position in the URL
- Access via params prop - automatically passed to page components
- Property name matches folder name - [slug] → params.slug
- Can have multiple dynamic segments - nested or at same level
- Use generateStaticParams - for pre-rendering at build time
- Always validate parameters - check if data exists, use notFound()
- TypeScript recommended - catch errors early with proper typing
What's Next?
You've mastered basic dynamic routes with single segments like [id]! But what if you need routes that match multiple segments or optional segments? That's where catch-all routes come in.
In the next lesson, we'll explore [...slug] and [[...slug]] patterns that give you even more routing flexibility—perfect for documentation sites, file browsers, and complex hierarchies!
🚀 Practice Makes Perfect
Dynamic routes are fundamental to most applications. Practice by building different types of pages: blog posts, user profiles, product details, and more. The patterns you learn here will serve you throughout your Next.js journey!