Understanding Routing in Next.js: A Comprehensive Guide
Next.js has revolutionized the way developers build React applications by providing powerful routing capabilities that are both flexible and intuitive. In this article, we'll explore the routing system in Next.js, with a special focus on dynamic routes and advanced routing concepts.
The Fundamentals of Next.js Routing
Next.js uses a file-system based router where:
- Files and folders in the
app
directory (App Router) orpages
directory (Pages Router) automatically become routes - Routes are rendered as UI components
- Nested routes follow the hierarchy of the file system
App Router vs Pages Router
Next.js 13 introduced the App Router, which coexists with the older Pages Router. Let's examine both:
App Router (app/ directory)
Plain Text1app/ 2āāā layout.js 3āāā page.js 4āāā about/ 5ā āāā page.js 6āāā blog/ 7 āāā layout.js 8 āāā page.js
- Uses React Server Components by default
- Supports layouts, nested layouts, and templates
- Provides more advanced routing patterns
Pages Router (pages/ directory)
Plain Text1pages/ 2āāā index.js 3āāā about.js 4āāā blog/ 5 āāā index.js
- The original routing system in Next.js
- Uses React Client Components by default
- Simpler structure but less powerful for complex applications
Dynamic Routes in Next.js
Dynamic routes allow you to create pages that can handle variable parameters. This is perfect for content-driven websites, blogs, e-commerce product pages, or any scenario where you need to generate pages based on data.
Creating Dynamic Routes
In the App Router
To create a dynamic route in the App Router, you use square brackets [param]
in your folder structure:
Plain Text1app/ 2āāā page.js 3āāā blog/ 4 āāā page.js 5 āāā [slug]/ 6 āāā page.js
In app/blog/[slug]/page.js
:
React JSX1export default function BlogPost({ params }) {
2 return <div>Blog Post: {params.slug}</div>;
3}
In the Pages Router
Similarly, in the Pages Router:
Plain Text1pages/ 2āāā index.js 3āāā blog.js 4āāā blog/ 5 āāā [slug].js
In pages/blog/[slug].js
:
React JSX1export default function BlogPost({ query }) {
2 const { slug } = query;
3 return <div>Blog Post: {slug}</div>;
4}
Accessing Route Parameters
App Router
The params
object is automatically passed to page components:
React JSX1export default function ProductPage({ params }) {
2 // params.productId contains the dynamic value
3 return <div>Product: {params.productId}</div>;
4}
Pages Router
The query
object from useRouter()
contains the route parameters:
React JSX1import { useRouter } from 'next/router';
2
3export default function ProductPage() {
4 const router = useRouter();
5 const { productId } = router.query;
6
7 return <div>Product: {productId}</div>;
8}
Nested Dynamic Routes
For more complex scenarios, you might need nested dynamic routes.
In the App Router
Plain Text1app/ 2āāā products/ 3 āāā [category]/ 4 āāā [productId]/ 5 āāā page.js
In app/products/[category]/[productId]/page.js
:
React JSX1export default function ProductPage({ params }) {
2 // Access both dynamic segments
3 return (
4 <div>
5 <h1>Category: {params.category}</h1>
6 <h2>Product ID: {params.productId}</h2>
7 </div>
8 );
9}
In the Pages Router
Plain Text1pages/ 2āāā products/ 3 āāā [category]/ 4 āāā [productId].js
In pages/products/[category]/[productId].js
:
React JSX1import { useRouter } from 'next/router';
2
3export default function ProductPage() {
4 const router = useRouter();
5 const { category, productId } = router.query;
6
7 return (
8 <div>
9 <h1>Category: {category}</h1>
10 <h2>Product ID: {productId}</h2>
11 </div>
12 );
13}
Catch-all and Optional Catch-all Routes
When you need to capture an arbitrary number of route segments, Next.js provides catch-all routes.
Catch-all Routes
App Router
Plain Text1app/ 2āāā blog/ 3 āāā [...slug]/ 4 āāā page.js
In app/blog/[...slug]/page.js
:
React JSX1export default function BlogPost({ params }) {
2 // params.slug is an array of path segments
3 // e.g., for /blog/2023/01/post-title, params.slug = ['2023', '01', 'post-title']
4 return (
5 <div>
6 <h1>Blog Post</h1>
7 <p>Path segments: {params.slug.join('/')}</p>
8 </div>
9 );
10}
Pages Router
Plain Text1pages/ 2āāā blog/ 3 āāā [...slug].js
Optional Catch-all Routes
Optional catch-all routes match even when the path segments don't exist.
App Router
Plain Text1app/ 2āāā docs/ 3 āāā [[...slug]]/ 4 āāā page.js
Pages Router
Plain Text1pages/ 2āāā docs/ 3 āāā [[...slug]].js
This matches /docs
, /docs/feature
, /docs/feature/concept
, etc.
Data Fetching for Dynamic Routes
One of the most common use cases for dynamic routes is fetching data for each route parameter.
In the App Router
React JSX1// app/products/[id]/page.js
2async function getProduct(id) {
3 const res = await fetch(`https://api.example.com/products/${id}`);
4 if (!res.ok) return null;
5 return res.json();
6}
7
8export default async function ProductPage({ params }) {
9 const product = await getProduct(params.id);
10
11 if (!product) {
12 return <div>Product not found</div>;
13 }
14
15 return (
16 <div>
17 <h1>{product.name}</h1>
18 <p>{product.description}</p>
19 <p>Price: ${product.price}</p>
20 </div>
21 );
22}
In the Pages Router
React JSX1// pages/products/[id].js
2import { useRouter } from 'next/router';
3import { useEffect, useState } from 'react';
4
5export default function ProductPage() {
6 const router = useRouter();
7 const { id } = router.query;
8 const [product, setProduct] = useState(null);
9 const [loading, setLoading] = useState(true);
10
11 useEffect(() => {
12 if (!id) return; // Router not ready yet
13
14 async function fetchProduct() {
15 try {
16 const res = await fetch(`https://api.example.com/products/${id}`);
17 if (!res.ok) throw new Error('Failed to fetch');
18 const data = await res.json();
19 setProduct(data);
20 } catch (error) {
21 console.error('Error fetching product:', error);
22 } finally {
23 setLoading(false);
24 }
25 }
26
27 fetchProduct();
28 }, [id]);
29
30 if (loading) return <div>Loading...</div>;
31 if (!product) return <div>Product not found</div>;
32
33 return (
34 <div>
35 <h1>{product.name}</h1>
36 <p>{product.description}</p>
37 <p>Price: ${product.price}</p>
38 </div>
39 );
40}
Generating Static Pages for Dynamic Routes
Next.js allows you to pre-render pages at build time, even for dynamic routes.
In the App Router
React JSX1// app/posts/[slug]/page.js
2export async function generateStaticParams() {
3 const posts = await fetchPosts();
4
5 return posts.map((post) => ({
6 slug: post.slug,
7 }));
8}
9
10async function fetchPost(slug) {
11 const res = await fetch(`https://api.example.com/posts/${slug}`);
12 if (!res.ok) return null;
13 return res.json();
14}
15
16export default async function Post({ params }) {
17 const post = await fetchPost(params.slug);
18
19 return (
20 <article>
21 <h1>{post.title}</h1>
22 <div dangerouslySetInnerHTML={{ __html: post.content }} />
23 </article>
24 );
25}
In the Pages Router
React JSX1// pages/posts/[slug].js
2export async function getStaticPaths() {
3 const posts = await fetchPosts();
4
5 return {
6 paths: posts.map((post) => ({
7 params: { slug: post.slug },
8 })),
9 fallback: 'blocking', // or false or true
10 };
11}
12
13export async function getStaticProps({ params }) {
14 const post = await fetchPost(params.slug);
15
16 if (!post) {
17 return { notFound: true };
18 }
19
20 return {
21 props: { post },
22 revalidate: 60, // Regenerate page every 60 seconds if requested
23 };
24}
25
26export default function Post({ post }) {
27 return (
28 <article>
29 <h1>{post.title}</h1>
30 <div dangerouslySetInnerHTML={{ __html: post.content }} />
31 </article>
32 );
33}
Parallel Routes and Intercepted Routes (App Router Only)
Next.js 13 introduced even more powerful routing capabilities.
Parallel Routes
Parallel routes allow you to simultaneously render multiple pages in the same layout.
Plain Text1app/ 2āāā layout.js 3āāā @dashboard/ 4ā āāā page.js 5āāā @analytics/ 6 āāā page.js
In app/layout.js
:
React JSX1export default function Layout({ children, dashboard, analytics }) {
2 return (
3 <div>
4 <div className="main">{children}</div>
5 <div className="dashboard">{dashboard}</div>
6 <div className="analytics">{analytics}</div>
7 </div>
8 );
9}
Intercepted Routes
Intercepted routes let you "intercept" a route and show different content while preserving the URL.
Plain Text1app/ 2āāā feed/ 3ā āāā page.js 4āāā photo/ 5 āāā [id]/ 6 ā āāā page.js 7 āāā (.)[id]/ 8 āāā page.js // This intercepts /photo/[id]
This is particularly useful for modals and specialized views.
Route Handlers (API Routes)
Next.js provides a way to create API endpoints as part of your application.
In the App Router
JavaScript1// app/api/products/route.js
2export async function GET() {
3 const products = await fetchProducts();
4 return Response.json(products);
5}
6
7export async function POST(request) {
8 const data = await request.json();
9 const newProduct = await createProduct(data);
10 return Response.json(newProduct, { status: 201 });
11}
In the Pages Router
JavaScript1// pages/api/products.js
2export default async function handler(req, res) {
3 if (req.method === 'GET') {
4 const products = await fetchProducts();
5 res.status(200).json(products);
6 } else if (req.method === 'POST') {
7 const newProduct = await createProduct(req.body);
8 res.status(201).json(newProduct);
9 } else {
10 res.setHeader('Allow', ['GET', 'POST']);
11 res.status(405).end(`Method ${req.method} Not Allowed`);
12 }
13}
Middleware for Route Processing
Next.js middleware allows you to run code before a request is completed, perfect for authentication, redirects, or rewriting URLs.
JavaScript1// middleware.js (at the root of your project)
2import { NextResponse } from 'next/server';
3
4export function middleware(request) {
5 // Check if user is authenticated
6 const isAuthenticated = checkAuth(request);
7
8 if (!isAuthenticated && request.nextUrl.pathname.startsWith('/dashboard')) {
9 return NextResponse.redirect(new URL('/login', request.url));
10 }
11
12 // Add headers
13 const response = NextResponse.next();
14 response.headers.set('x-custom-header', 'custom-value');
15 return response;
16}
17
18export const config = {
19 matcher: ['/dashboard/:path*', '/api/:path*'],
20};
URL Handling and Query Parameters
Next.js provides utilities for working with URLs and query parameters.
In the App Router
React JSX1// app/search/page.js
2export default function SearchPage({ searchParams }) {
3 const query = searchParams.query || '';
4 const page = searchParams.page ? parseInt(searchParams.page) : 1;
5
6 return (
7 <div>
8 <h1>Search Results for: {query}</h1>
9 <p>Page: {page}</p>
10 {/* Search results */}
11 </div>
12 );
13}
In the Pages Router
React JSX1// pages/search.js
2import { useRouter } from 'next/router';
3
4export default function SearchPage() {
5 const router = useRouter();
6 const { query = '', page = '1' } = router.query;
7
8 return (
9 <div>
10 <h1>Search Results for: {query}</h1>
11 <p>Page: {parseInt(page)}</p>
12 {/* Search results */}
13 </div>
14 );
15}
Navigation Between Routes
Next.js provides client-side navigation between routes without full page refreshes.
Using the Link Component
React JSX1import Link from 'next/link';
2
3export default function Navigation() {
4 return (
5 <nav>
6 <Link href="/">Home</Link>
7 <Link href="/about">About</Link>
8 <Link href={`/blog/${postSlug}`}>Read Post</Link>
9 <Link href="/products?category=electronics">Electronics</Link>
10 </nav>
11 );
12}
Programmatic Navigation
React JSX1// App Router
2'use client';
3
4import { useRouter } from 'next/navigation';
5
6export default function LoginForm() {
7 const router = useRouter();
8
9 const handleSubmit = async (e) => {
10 e.preventDefault();
11 const success = await login(/* credentials */);
12
13 if (success) {
14 router.push('/dashboard');
15 }
16 };
17
18 return (/* form */);
19}
20
21// Pages Router
22import { useRouter } from 'next/router';
23
24export default function LoginForm() {
25 const router = useRouter();
26
27 const handleSubmit = async (e) => {
28 e.preventDefault();
29 const success = await login(/* credentials */);
30
31 if (success) {
32 router.push('/dashboard');
33 }
34 };
35
36 return (/* form */);
37}
Error Handling in Routes
Next.js provides built-in error handling for routes.
In the App Router
React JSX1// app/products/[id]/error.js
2'use client';
3
4export default function Error({ error, reset }) {
5 return (
6 <div>
7 <h2>Something went wrong loading this product!</h2>
8 <p>{error.message}</p>
9 <button onClick={() => reset()}>Try again</button>
10 </div>
11 );
12}
In the Pages Router
React JSX1// pages/_error.js
2function Error({ statusCode }) {
3 return (
4 <p>
5 {statusCode
6 ? `An error ${statusCode} occurred on server`
7 : 'An error occurred on client'}
8 </p>
9 );
10}
11
12Error.getInitialProps = ({ res, err }) => {
13 const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
14 return { statusCode };
15};
16
17export default Error;
Loading States
Next.js also provides built-in loading states for routes.
In the App Router
React JSX1// app/products/loading.js
2export default function Loading() {
3 return <div>Loading products...</div>;
4}
Best Practices for Next.js Routing
Organize Routes Logically: Structure your routes to reflect the information hierarchy of your application.
Use Layouts Effectively: Leverage layouts to avoid code duplication across related routes.
Optimize Data Fetching: Fetch data at the appropriate level and cache it when possible.
Handle Loading and Error States: Always provide good user experiences during loading and error situations.
Use Static Generation When Possible: Pre-render pages at build time when the content doesn't change frequently.
Implement Proper SEO: Use metadata, proper titles, and descriptions for all routes.
Secure Routes Appropriately: Use middleware for authentication and authorization checks.
Test Your Routes: Ensure your routing works correctly across different scenarios and edge cases.
Conclusion
Next.js provides a powerful and flexible routing system that can handle everything from simple static pages to complex dynamic routes. By understanding the various routing features and patterns available in Next.js, you can build well-structured, performant, and user-friendly web applications.
Whether you're using the newer App Router or the classic Pages Router, Next.js offers tools to create intuitive navigation, handle data fetching, manage loading and error states, and optimize performance. As your application grows in complexity, these routing capabilities become even more valuable for maintaining a clean architecture and excellent user experience.