If you've ever built a website with more than a handful of pages, you know the pain of creating a separate file for every single piece of content. Imagine an e-commerce store with 10,000 products—you wouldn't build 10,000 individual pages. That's where Next.js dynamic routing comes in. It's a core feature that lets you create a single, flexible template to handle countless pages.
This approach is the backbone of any scalable application, whether it's a blog, a marketplace, or a social platform where new content is always being added.
What Is Dynamic Routing in Next JS?
Think of it like this: a static route is a fixed address. A URL like /about will always take you to the exact same "About Us" page. It’s predictable and unchanging.
Dynamic routing, on the other hand, is like having a customizable address template. You define a pattern, and Next.js fills in the blanks. For example, a route like /products/[productId] uses [productId] as a placeholder, or what Next.js calls a dynamic segment.
When a user navigates to /products/cool-blue-shirt, Next.js grabs your product page template and uses "cool-blue-shirt" to fetch and display the right data. If they go to /products/fancy-red-shoes, it's the same template, just populated with different information. You write the code once, and it works for every product you’ll ever have.
The Power of Convention Over Configuration
The real magic here is how simple Next.js makes this. You don't need to wrestle with complex configuration files or third-party routing libraries. Next.js uses file-system-based routing, meaning the structure of your folders and files is your routing structure. It's an intuitive "convention over configuration" approach that just works.
This system brings some huge wins:
- Scalability: Manage millions of pages from just a few template files.
- Maintainability: Need to update the layout for every blog post? Just edit one file.
- SEO-Friendliness: Dynamic routing helps you create clean, descriptive URLs like
/blog/how-to-build-great-appsthat search engines love.
We'll dig into how this plays out in both the classic Pages Router and the modern App Router. The core idea is consistent, but as Next.js has evolved, so have its routing capabilities. If you're curious about the bigger picture, check out our overview of Next.js routing strategies.
Dynamic Routing in Next JS Pages vs App Router At a Glance
The way you create dynamic routes has changed between the old Pages Router and the new App Router. The fundamental concept is the same, but the syntax and file structure are different. Knowing the distinction is crucial, especially if you're migrating an older project or starting fresh.
Let's break down the key differences with a quick comparison table. This will give you a clear snapshot of how things work in both worlds.
| Feature | Pages Router (Legacy) | App Router (Modern) |
|---|---|---|
| Syntax | pages/blog/[slug].js |
app/blog/[slug]/page.js |
| Parameter Access | const router = useRouter(); const { slug } = router.query; |
export default function Page({ params }) { const { slug } = params; } |
| Data Fetching | getStaticProps, getServerSideProps |
async Server Components, generateStaticParams |
| Organization | Flat file structure within pages directory. |
Co-location of UI, data, and test files within route folders. |
Key Takeaway: The biggest shift is from file-based routes (
[id].js) to folder-based routes ([id]/page.js). The App Router's approach is a game-changer for organization, as it lets you keep everything related to a route—layouts, loading states, error boundaries, and components—all neatly tucked into one folder.
Creating Dynamic Segments in the Pages Router
For a long time, the Pages Router was the heart of every Next.js application. Its approach to handling Next.js dynamic routing is still incredibly intuitive and powerful, all thanks to a clever file-naming convention.
To make a route dynamic, you just wrap the filename in square brackets. That's it. For instance, creating a file at pages/blog/[slug].js instantly sets up a template that can handle any URL like /blog/hello-world, /blog/my-next-big-thing, or anything else you throw at it. The [slug] part is a placeholder that captures whatever comes after /blog/.
This simple convention means you can run a blog with thousands of articles from a single file. Next.js grabs the value from the URL (like "hello-world") and passes it into your page component, so you can fetch the right data and show the correct content.
Accessing URL Parameters
Okay, so how do you actually get your hands on that "hello-world" value inside your code? You'll want to reach for the useRouter hook, which comes directly from next/router. This hook is your gateway to the router object, which holds everything you need to know about the current URL, including those dynamic bits.
Here’s the basic flow:
- Import the Hook: First, pull in
useRouterat the top of your page component. - Initialize it: Call the hook inside your component to get the
routerinstance. - Grab the Query: The
routerobject has aqueryproperty. This is where your URL parameters live.
Let's see what this looks like in our pages/blog/[slug].js example:
import { useRouter } from 'next/router';
const PostPage = () => {
const router = useRouter();
const { slug } = router.query;
return
Viewing Blog Post: {slug}
;};
export default PostPage;
With this code, if someone navigates to /blog/my-first-post, the slug variable becomes the string "my-first-post". The page will then render the heading "Viewing Blog Post: my-first-post". Simple and effective.
Expanding with Advanced Patterns
Of course, the real world often needs more than just a single dynamic segment. What about documentation sites with nested sections or e-commerce stores with deep category trees? The Pages Router has you covered with a couple of advanced patterns.
Key Insight: Getting comfortable with these advanced routing patterns is what separates a basic site from a truly flexible and scalable application. A simple
[slug]works wonders for a blog, but catch-all routes open up a whole new world of possibilities for handling complex URL hierarchies.
Here are the two powerhouse variations you'll definitely want to know:
Catch-all Routes
[...slug].js: This pattern is greedy—it grabs all subsequent URL segments and bundles them into an array. If you have a file atpages/docs/[...slug].js, it would match/docs/getting-started/installation. The router would then give yourouter.query.slugas an array:['getting-started', 'installation'].Optional Catch-all Routes
[[...slug]].js: This one is just like the catch-all, but with a twist: it also matches the base route. A file namedpages/shop/[[...slug]].jswill handle/shop,/shop/clothing, and/shop/clothing/shirts. If a user visits just/shop, theslugparameter will simply be undefined.
These file-naming rules, when paired with data-fetching functions like getStaticPaths and getServerSideProps, give you total control over how your dynamic pages are rendered. You can pre-build them at compile time or render them on-demand for each request, striking the perfect balance between performance and flexibility.
How the App Router Modernizes Dynamic Routing
With the introduction of the App Router, Next.js completely re-imagined its routing system. This wasn't just a minor update; it was a fundamental shift built to fully embrace React Server Components. The core principle of dynamic routing moved from file-based to folder-based. So, where you once had a file like [slug].js, you now create a folder named [productId] and place a page.js file inside it.
This folder-centric model is a huge win for project organization. It lets you co-locate everything related to a specific route—the page itself, components, tests, and even styles—all neatly tucked away in one directory. As your application scales, this approach keeps your codebase clean and much easier to navigate.
A New Way to Access Parameters
One of the best quality-of-life improvements is how you get access to URL parameters. In the old Pages Router, you had to pull in the useRouter hook just to grab an ID from the URL. The App Router simplifies this dramatically by passing dynamic segments directly into your page component as props.
For a route like app/products/[productId]/page.js, the productId is automatically available.
// Located at app/products/[productId]/page.js
export default function ProductPage({ params }) {
// The 'productId' from the URL is available directly in params
const { productId } = params;
return
Details for Product: {productId}
;}
This is so much cleaner. It's more predictable and makes data access straightforward, especially in Server Components where you can't use hooks anyway.
The diagram below shows the old Pages Router flow, where the filename directly defined the URL. It’s a great visual contrast to the new folder-based system.
This image really highlights the direct file-to-URL mapping that we were used to, a convention that has now evolved into the App Router's more powerful, structured paradigm.
Unlocking Advanced Routing Structures
The App Router's folder-based design also makes it far easier to handle complex routing patterns. Since every route is a directory, you can nest them intuitively to create deep, layered dynamic URLs without any hassle.
Key Advantage: The App Router’s folder structure is a game-changer for building sophisticated apps. It natively supports nested layouts and parallel routes, which means different parts of your UI can have their own routing logic while still sharing a common parent layout.
This architectural leap has been a massive driver of adoption since its stable release in 2023. While the Pages Router had a relatively flat structure, the App Router excels at creating complex nested routes like /dashboard/[team]/analytics/[metric]. These routes can share layouts and data-fetching logic, which is a huge benefit for building enterprise-grade applications. The proof is in the numbers: npm downloads for Next.js have exploded by over 500% in the last five years, with developers across the US, EU, and Asia quickly adopting the new paradigm. You can dig deeper into the benefits of this modern Next.js architecture to see how it's changing development workflows.
This new model isn't just about better organization; it has a direct impact on performance. By leaning on Server Components for data fetching within these dynamic routes, you ship less JavaScript to the client, resulting in faster initial page loads and a snappier user experience. It's a modern approach for modern web applications.
Advanced Data Fetching for Dynamic Pages
A dynamic route is really just an empty picture frame. It gives you the structure, but the data you fetch is what fills it in and makes it useful. This is where Next.js dynamic routing truly comes alive, blending perfectly with its data-fetching strategies to build high-performance, data-driven web pages.
Of course, how you grab that data depends entirely on which router you're using. The classic Pages Router and the modern App Router each have their own way of doing things, with specific functions designed to get the job done right. Let's break down both approaches.
Data Fetching in the Pages Router
When you're working with the Pages Router, getStaticPaths is your go-to function for pre-rendering dynamic pages at build time. Think of it as giving a guest list to a party host. You’re telling Next.js exactly which pages to prepare for ahead of time.
This function doesn't work alone; it's the partner to getStaticProps, which is responsible for fetching the actual data for each page on that list.
getStaticPaths needs to return an object with two critical properties:
paths: This is an array of objects, and each object defines theparamsfor a specific route. For a dynamic route like[slug].js, it would look something like[{ params: { slug: 'post-one' } }].fallback: A boolean or string that tells Next.js what to do when a user requests a path that wasn't on your pre-built list. Setting it tofalsewill show a 404 page. Usingtrueor'blocking'enables on-demand page generation for new paths.
Key Takeaway: The
fallbackproperty is your control knob for balancing build times against flexibility. For a small, finite set of pages (like a portfolio),fallback: falseis perfect. But for a massive e-commerce site or a blog with constantly new content,fallback: trueis a lifesaver.
The Modern App Router Approach
The App Router takes a much more direct approach, thanks to the power of async Server Components. The equivalent of getStaticPaths here is a new function called generateStaticParams. It does the same job—defining which dynamic segments to pre-render—but in a more streamlined and intuitive way.
Here’s a quick look at how it works inside app/blog/[slug]/page.js:
// Tell Next.js which blog posts to pre-render
export async function generateStaticParams() {
const posts = await fetch('https://…/posts').then((res) => res.json());
return posts.map((post) => ({
slug: post.slug,
}));
}
// Fetch data for a specific post right in the component
export default async function Page({ params }) {
const post = await getPostBySlug(params.slug);
// … render page content
}
See how much cleaner that is? You just export an async function that returns the params, and the page component itself becomes an async function that fetches its own data. This co-location makes the code far easier to follow and maintain. For those using different data sources, our guide on how to fetch GraphQL data in Next.js dives into patterns that fit this model perfectly.
Choosing Your Rendering Strategy
Once your data fetching is sorted, the last piece of the puzzle is deciding how your pages will be rendered and updated. Modern caching and on-demand revalidation, especially with functions like revalidatePath, give you incredible control.
For an e-commerce site, you could call revalidatePath() for a specific [productId] route to refresh stock levels in seconds, all without a full site rebuild. This kind of granular control is powerful; some industry benchmarks have shown that techniques like this can lift cart abandonment rates by as much as 15%. To see how this works in practice, you can explore the benefits of Next.js caching.
Linking and Navigating to Dynamic Routes
So, you've set up your dynamic pages, but how do you get users to them? Creating the routes is only half the job. You need a way for people to actually click around and explore your content.
This is where Next.js really shines. It gives you powerful tools for client-side navigation that make moving between pages feel incredibly fast and seamless—a critical part of any great user experience. Instead of clunky, full-page reloads, users get a smooth, app-like feel.
Using the Link Component
The hero of Next.js navigation is the <Link> component. On the surface, it looks and acts like a regular <a> tag, but it’s doing a ton of work behind the scenes. Its main jobs are enabling those buttery-smooth client-side transitions and prefetching pages, so your app feels responsive and snappy.
Building a path for the <Link> component's href prop is surprisingly simple. For a Next.js dynamic routing setup like /blog/[slug], you can just use template literals to piece the URL together. It’s clean, easy to read, and works perfectly for most scenarios.
Here’s what that looks like in a real-world example, generating a list of blog post links:
import Link from 'next/link';
const BlogList = ({ posts }) => {
return (
{/* Using a template literal for a clean, dynamic href */}
<Link href={/blog/${post.slug}}>
{post.title}
{posts.map((post) => (
))}
);
};
This snippet creates a list where each link points to a unique, dynamically generated blog page. Even better, when a user just hovers over one of these links, Next.js automatically starts prefetching the page's data in the background, making the actual click feel almost instant.
And things are only getting better. By 2026, with the full rollout of Next.js 16's routing overhaul, new layout deduplication will massively reduce network traffic. Imagine prefetching 50 product links; instead of downloading the shared layout 50 times, it will be downloaded just once. This could slash bandwidth by up to 98% on pages with lots of links. You can discover more about these upcoming routing improvements on the official blog.
Navigating Programmatically with useRouter
What about when you need to send a user somewhere automatically? Think about redirecting after a form submission or a successful login. This is called programmatic navigation, and the useRouter hook from next/navigation is the tool for the job.
Key Takeaway: The
<Link>component is for when a user decides to navigate by clicking something. TheuseRouterhook is for when your code needs to trigger the navigation itself, based on some event or logic.
Using it is straightforward: import the hook inside a client component, call it to get the router object, and then use router.push() with the path you want to go to. This hands you the keys to control the navigation flow from right inside your application logic.
For instance, you could send a user to a "thank you" page right after they submit a contact form:
'use client';
import { useRouter } from 'next/navigation';
export default function ContactForm() {
const router = useRouter();
const handleSubmit = async (event) => {
event.preventDefault();
// Logic to submit the form data…
router.push('/thank-you');
};
return {/* Form fields */};
}
By getting comfortable with both the <Link> component and the useRouter hook, you can build a fast, intuitive, and fully connected experience around all your dynamic content.
Common Pitfalls and Best Practices
Getting the hang of Next.js dynamic routing feels like a superpower, but with that power comes a few classic ways to trip up. Knowing where the landmines are is the key to a smooth development process instead of a frustrating one. Let's walk through the common mistakes I've seen and the best practices that will help you build tougher, faster, and more secure apps.
The "Oops, Wrong Folder" Mistake
One of the most frequent hiccups, especially for folks coming from the Pages Router, is a simple file naming error. It's so easy to create a file like app/blog/[slug].js out of habit. But in the App Router, that's not how it works anymore.
The new convention demands a folder for the dynamic part. The right way is app/blog/[slug]/page.js. This tiny change is probably the number one reason developers see "404 Not Found" errors when they're getting used to the App Router.
Another classic issue, this time in the Pages Router, is with getStaticProps. If you try to return data that can't be turned into JSON—like a Date object or a function—Next.js will flat-out reject it with an error. Remember, everything you pass as props has to be a simple, serializable data type.
Making Your Dynamic Pages Shine with SEO and Security
Once you're past the initial hurdles, you can start making your dynamic pages truly powerful. A huge part of that is making them search engine friendly. In the App Router, the generateMetadata function is your best friend here. It lets you programmatically create unique titles, descriptions, and Open Graph tags for every single generated page.
Think of it this way: a well-built dynamic route is a massive SEO win. When you generate metadata for every blog post, product, or user profile, you're telling search engines exactly what each page is about. This massively boosts your site's discoverability.
Security is non-negotiable. Never, ever trust URL parameters directly. Always clean up and validate dynamic segments like [id] or [slug] before they even get close to a database query. This is your first and best defense against nasty stuff like injection attacks. If a parameter looks fishy—say, it's not a proper number or UUID—just show a 404 page. You can learn more about creating a custom 404 page in Next.js to handle this gracefully.
Pushing for Peak Performance
Great performance comes down to smart data fetching and solid error handling. We're seeing some amazing results out in the wild. For example, companies like Inngest have reported 75% faster load times on dynamic dashboards just by moving to the App Router.
At the same time, developers are finding that placing loading.js and error.js files right inside their dynamic route folders can slash their state management code by up to 60%. That's less boilerplate and a much better user experience. You can discover more insights about Next.js benefits and migration stories to see how it's playing out for other teams.
Always make sure you have proper loading and error states in place. It's what separates a professional, polished app from a clunky one, especially when data is taking a second to load or something goes wrong behind the scenes.
Frequently Asked Questions About Dynamic Routing
As you get your hands dirty with Next.js dynamic routing, a few common questions tend to pop up. Let's tackle them head-on with some quick, clear answers to help you solve common problems and really cement your understanding.
How Do I Handle 404 Pages for Dynamic Routes That Don't Exist?
This is a classic problem. You've set up a dynamic route, but what happens when a user tries to visit a URL for a post or product that isn't there?
In the Pages Router, you'd typically return { notFound: true } from getStaticProps or getServerSideProps. This tells Next.js to serve up your 404 page. If you're using fallback: true in getStaticPaths, you'll also need to add some logic inside your page component to handle the case where the data fetching comes back empty.
The App Router makes this much cleaner. You just call the notFound() function from next/navigation right inside your component, usually after a failed data fetch. Next.js then automatically finds and renders the closest not-found.js file up the directory tree. It's a much more direct way to handle things.
What Is the Difference Between a Slug and a Catch-All Route?
The core difference here is all about greed—how many URL segments a route tries to capture.
A standard dynamic segment, like
[slug], is precise. It matches just one part of the URL. For example, a route file at/blog/[slug].jswill match/blog/my-first-postbut not/blog/my/first/post.A catch-all route,
[...slug], is greedy. It matches everything that follows. A route like/docs/[...slug].jswould happily match/docs/getting-started/installation, and theslugparameter would be an array:['getting-started', 'installation'].
Think of [slug] as grabbing a single item, while [...slug] grabs a whole shopping basket of URL segments.
Can I Use Dynamic Routing with Both SSG and SSR?
Absolutely. This is one of the most powerful features in Next.js. You aren’t locked into a single rendering strategy.
With the Pages Router, you can use getStaticPaths to pre-build all your dynamic pages at build time (SSG), or you can use getServerSideProps to render them on-demand for every request (SSR).
The App Router carries this flexibility forward. You can use generateStaticParams to get the same SSG behavior, pre-rendering your dynamic routes for blazing-fast loads. Or, you can make a route dynamic by simply using a dynamic function like cookies() or headers(), which tells Next.js to render it on the server at request time (SSR).
This per-page control is a game-changer. We've seen teams use this to launch feature pages in just days, boosting conversions by 20-30% simply by choosing the right high-performance rendering strategy. You can discover more about Next.js 16's routing improvements to see how these features have evolved.
At Next.js & React.js Revolution, we publish daily guides and tutorials to help you master modern web development. Explore our resources to build better, faster applications today at https://nextjsreactjs.com.
