Modern web development demands applications that are not only scalable and performant, but also able to deliver customized, dynamic experiences based on user input and data. Dynamic routing is a crucial technique to achieve these goals, allowing frameworks like NextJS to generate pages on-the-fly as a user navigates. Whether you're architecting a complex SaaS dashboard, building e-commerce with React.js, or orchestrating backends with Django and Docker, mastering dynamic routing in NextJS is essential for robust system design.
This article provides a comprehensive technical walkthrough of dynamic routing in NextJS, blending conceptual clarity, practical code, real-world patterns, and an analysis of trade-offs.
In the context of web development, routing refers to the process of determining how an application responds to a given URL path (such as /users/42) by rendering a particular piece of code, typically a page or an API endpoint. Routing can be:
/about, /contact)./users/[id], where [id] can be any value).
Dynamic routing in NextJS allows developers to define routes whose segments are dynamic variables, enabling the application to handle requests like /posts/hello-nextjs or /products/12345 by extracting the path segments and providing context to the rendered page.
Unlike traditional routing in frameworks like Django (which often relies on explicitly defined patterns), NextJS leverages file-system based routing: the folder/file structure inside the pages/ directory defines the available routes in your application. Dynamic segments are specified using square brackets.
/pages (or /app in newer NextJS projects).[id]), defined in the file system as pages/users/[id].js to handle all /users/... requests.[...slug].[[...slug]], which also matches the route if those segments are missing.
When a request hits a dynamic route, NextJS parses the path segment, injects it into the page's getServerSideProps or getStaticProps (for static generation), and delivers a component with access to this data via props or router hooks. This enables both server-side data fetching (think of querying a Django REST API running in a Dockerized backend) and client-side interactivity (with React.js state management).
Suppose you want URLs like /products/<productId> for an e-commerce app. You would create a file:
/pages/products/[productId].js
The [productId] part tells NextJS this segment is dynamic. When a user visits /products/12345, productId will be 12345.
Inside your page component, you can access parameters in several ways.
useRouter (Client-side)
import { useRouter } from "next/router";
export default function ProductPage() {
const router = useRouter();
const { productId } = router.query;
// You can use `productId` to fetch data, etc.
return (
<div>
<h2>Product ID: {productId}</h2>
</div>
);
}
getServerSideProps or getStaticProps (Server/Static Side)
// pages/products/[productId].js
export async function getServerSideProps(context) {
const { productId } = context.params;
// Fetch product from database or API
const res = await fetch(`https://api.myapp.com/products/${productId}`);
const product = await res.json();
return {
props: { product }, // Passed to the page component as props
};
}
export default function ProductPage({ product }) {
return (
<div>
<h2>{product.name}</h2>
<p>Price: ${product.price}</p>
</div>
);
}
This approach is particularly suitable for integrating with backend frameworks like Django running in Docker containers.
/users/username where username is dynamic.
/blog/[slug] where [slug] can be any article id or title.
/catalog/[category]/[item] allow for arbitrary category/item navigation.
/api/[...params].js process requests with arbitrary paths.
A catch-all route matches any number of path segments. File name: [...slug].js.
// pages/docs/[...slug].js
import { useRouter } from 'next/router';
export default function DocsPage() {
const router = useRouter();
const { slug } = router.query; // `slug` is an array
return (
<div>
<h2>Docs path: {Array.isArray(slug) ? slug.join(" / ") : ""}</h2>
</div>
);
}
Visiting /docs/nextjs/dynamic-routing will give slug = ['nextjs', 'dynamic-routing'].
An optional catch-all route lets even the base path (/docs) match by naming the file [[...slug]].js.
getStaticPaths)
For performance and SEO, it's common to statically generate pages at build time (SSG - Static Site Generation). NextJS does this with getStaticProps and getStaticPaths. While getStaticProps fetches data for a page, getStaticPaths tells NextJS which dynamic routes to pre-render.
// pages/products/[productId].js
export async function getStaticPaths() {
// Fetch the list of products from an API, DB, or filesystem
const res = await fetch('https://api.myapp.com/products');
const products = await res.json();
// Pre-render only these paths at build time
const paths = products.map(product => ({
params: { productId: product.id.toString() }
}));
return { paths, fallback: "blocking" };
}
This is often paired with getStaticProps to fetch product data per page. Fallback modes dictate how "missing" paths are handled (serve 404, generate on-demand, etc.).
getServerSideProps), ideal if data is always changing (e.g., tied to a Django backend API containerized by Docker).Dynamic routing must be balanced with caching/CDN strategy, especially if you are containerizing apps with Docker for microservice-style deployments.
pages/404.js to customize "not found" responses for invalid dynamic routes.Imagine you run a Docker Compose setup with:
You want /products/[productId] to show product details fetched live from the Django API.
// docker-compose.yml (simplified)
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
depends_on:
- backend
environment:
- API_URL=http://backend:8000
backend:
build: ./backend
ports:
- "8000:8000"
// pages/products/[productId].js
export async function getServerSideProps({ params, req }) {
const baseUrl = process.env.API_URL || "http://localhost:8000";
const res = await fetch(`${baseUrl}/api/products/${params.productId}/`);
const product = await res.json();
if (!product) {
return { notFound: true };
}
return { props: { product } };
}
This tightly decouples frontend and backend. React.js components display the fetched data. The system scales effortlessly: container orchestration with Docker, fast static generation or SSR with NextJS, and robust APIs via Django.
Request Flow: User Visit (→) NextJS Server (→) Detects Dynamic Route (/products/42) (→) Fetches Data from Django REST API (in Docker) (→) Returns Assembled Page to Browser.
/products/[productId] = [products folder] → [dynamic file: [productId].js] → [page component with data props]
getServerSideProps (dynamic) or getStaticProps with revalidate (ISR) accordingly./docs, /docs/topic, etc.).
Dynamic routing in NextJS is engineered for developer productivity, high scalability, and user-centric design, especially when integrated with robust React.js frontends, Django REST backends, and Dockerized deployment pipelines. Key takeaways include understanding the file-system-based routing model, mastering dynamic segment syntax, leveraging data fetching methods (getServerSideProps, getStaticProps + getStaticPaths), and architecting for performance/caching in real-world distributed systems. Errors, edge cases, and security are critical to scalable implementations.
To grow beyond, experiment with:
Apply these concepts to your next system design project, whether it's a React.js app, Django REST API, or a distributed, containerized platform. Dynamic routing is not just a technique—it's a cornerstone of scalable, maintainable, and user-driven web applications.
