Skip to main content

Next.js Dynamic Routes

Introduction

In web applications, you often need to create pages that display content based on dynamic parameters. For instance, when building a blog, you might want URLs like /posts/how-to-learn-nextjs or /posts/understanding-react-hooks. Rather than creating a separate page file for each blog post, Next.js allows you to create dynamic routes that can handle variable paths.

Dynamic routes enable you to build pages that can capture values from the URL, making your application more flexible and maintainable. In this tutorial, we'll dive into how dynamic routes work in Next.js, explore their capabilities, and implement them in practical examples.

Understanding Dynamic Routes

What Are Dynamic Routes?

Dynamic routes in Next.js allow you to create pages that match variable paths. Instead of defining a static route like /posts/first-post and /posts/second-post, you can create a single dynamic route that matches multiple paths following a pattern.

How Dynamic Routes Work in Next.js

In Next.js, you can create dynamic routes by adding brackets [] to a page file or folder name. The value inside the brackets becomes a parameter that's accessible in your page component.

Basic Dynamic Route Implementation

Creating a Dynamic Route

Let's create a simple dynamic route for blog posts:

  1. Create a file structure in your Next.js project:
app/
└── blog/
└── [slug]/
└── page.js
  1. Implement the page component:
jsx
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return (
<div>
<h1>Blog Post</h1>
<p>You are viewing the blog post: {params.slug}</p>
</div>
);
}

Now, when users visit /blog/hello-world or /blog/learn-nextjs, they'll see the same component with the respective slug parameter.

How Parameters Are Passed

In the App Router, parameters are automatically passed to your page component via the params prop. The name of the parameter corresponds to the name used in the brackets.

Working with Dynamic Route Parameters

Accessing Route Parameters

You can access route parameters in your page components through the params object:

jsx
// app/products/[id]/page.js
export default function ProductPage({ params }) {
// params.id contains the dynamic value
return (
<div>
<h1>Product Details</h1>
<p>Product ID: {params.id}</p>
</div>
);
}

Using Parameters to Fetch Data

A common use case for dynamic routes is fetching data based on the route parameter:

jsx
// app/users/[userId]/page.js
async function getUser(userId) {
const res = await fetch(`https://api.example.com/users/${userId}`);

if (!res.ok) {
throw new Error('Failed to fetch user');
}

return res.json();
}

export default async function UserProfile({ params }) {
const user = await getUser(params.userId);

return (
<div>
<h1>{user.name}'s Profile</h1>
<img src={user.avatar} alt={user.name} />
<p>Email: {user.email}</p>
<p>Member since: {new Date(user.joinedAt).toLocaleDateString()}</p>
</div>
);
}

Multiple Dynamic Segments

Nested Dynamic Routes

Next.js allows you to create nested dynamic routes by using multiple dynamic segments:

app/
└── products/
└── [category]/
└── [id]/
└── page.js

In this example, you can access both the category and id parameters:

jsx
// app/products/[category]/[id]/page.js
export default function ProductPage({ params }) {
return (
<div>
<h1>Product Details</h1>
<p>Category: {params.category}</p>
<p>Product ID: {params.id}</p>
</div>
);
}

This page will match URLs like /products/electronics/12345 or /products/books/harry-potter.

Catch-All Routes

For even more flexible routing, Next.js provides catch-all routes using the spread syntax ([...param]):

app/
└── docs/
└── [...slug]/
└── page.js

This will match any number of path segments under /docs/:

jsx
// app/docs/[...slug]/page.js
export default function DocsPage({ params }) {
// If URL is /docs/getting-started/installation
// params.slug will be ["getting-started", "installation"]

return (
<div>
<h1>Documentation</h1>
<p>You are viewing: {params.slug.join('/')}</p>
</div>
);
}

Optional Catch-All Routes

To make a catch-all route optional, you can use double brackets ([[...param]]):

app/
└── dashboard/
└── [[...settings]]/
└── page.js

This will match /dashboard and any paths under it like /dashboard/profile/preferences:

jsx
// app/dashboard/[[...settings]]/page.js
export default function DashboardPage({ params }) {
// For URL /dashboard - params.settings will be undefined
// For URL /dashboard/profile - params.settings will be ["profile"]
// For URL /dashboard/profile/preferences - params.settings will be ["profile", "preferences"]

const settingsPath = params.settings ? params.settings.join('/') : 'main dashboard';

return (
<div>
<h1>Dashboard</h1>
<p>Viewing: {settingsPath}</p>
</div>
);
}

Generating Static Pages for Dynamic Routes

Static Generation with generateStaticParams

For improved performance, you can pre-render pages with specific parameters using generateStaticParams:

jsx
// app/posts/[slug]/page.js
export async function generateStaticParams() {
const posts = await getPosts(); // Function to fetch all posts

return posts.map((post) => ({
slug: post.slug,
}));
}

export default function Post({ params }) {
return <PostContent slug={params.slug} />;
}

This will pre-generate pages for all the slugs returned by generateStaticParams at build time, making your site faster.

Practical Example: Building a Blog

Let's build a simple blog using dynamic routes:

jsx
// app/blog/[slug]/page.js
import { getBlogPost } from '@/lib/blog';

export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());

return posts.map((post) => ({
slug: post.slug,
}));
}

export default async function BlogPost({ params }) {
const post = await getBlogPost(params.slug);

if (!post) {
return <div>Post not found</div>;
}

return (
<article>
<h1>{post.title}</h1>
<div className="metadata">
<span>Published on: {new Date(post.date).toLocaleDateString()}</span>
<span>By: {post.author}</span>
</div>
<div className="content" dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}

Handling Not Found Routes

You can create custom not-found pages for dynamic routes:

jsx
// app/blog/[slug]/page.js
import { notFound } from 'next/navigation';

async function getBlogPost(slug) {
const res = await fetch(`https://api.example.com/posts/${slug}`);

if (!res.ok) {
return null;
}

return res.json();
}

export default async function BlogPost({ params }) {
const post = await getBlogPost(params.slug);

if (!post) {
notFound();
}

return (
<article>
<h1>{post.title}</h1>
{/* Post content */}
</article>
);
}

Then create a not-found page:

jsx
// app/blog/[slug]/not-found.js
export default function NotFound() {
return (
<div>
<h1>Blog Post Not Found</h1>
<p>Sorry, the blog post you're looking for doesn't exist.</p>
</div>
);
}

Summary

Dynamic routes are a powerful feature in Next.js that allow you to:

  • Create flexible URL structures without hardcoding each path
  • Access URL parameters in your components
  • Implement complex routing patterns with nested and catch-all routes
  • Pre-render pages for specific parameter values
  • Build SEO-friendly applications with dynamic content

By mastering dynamic routes, you can create more maintainable, scalable, and user-friendly web applications.

Additional Resources

Exercises

  1. Create a dynamic route for a product catalog where users can filter products by category and subcategory (e.g., /products/electronics/laptops).

  2. Implement a documentation site with nested dynamic routes that displays different sections and subsections.

  3. Build a blog with dynamic routes that includes pagination and filtering by tags.

  4. Create a user profile system with dynamic routes that display user information and posts created by that user.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)