Chapter 4: Routing and Navigation in Next.js
What is Next.js Routing?
Next.js uses a file-based routing system where the file structure in the pages/
directory automatically determines the routes of your application. This approach eliminates the need for manual route configuration and provides a intuitive way to organize your application's navigation.
Key Concepts:
- File-based Routing: Each file in
pages/
becomes a route - Automatic Code Splitting: Each page is automatically code-split
- Nested Routing: Folders create nested routes
- Dynamic Routing: Square brackets
[]
create dynamic routes - API Routes: Files in
pages/api/
become API endpoints
Why Use File-based Routing?
Advantages:
- Zero Configuration: No need to set up routing manually
- Intuitive Structure: File location directly maps to URL
- Automatic Code Splitting: Each page loads only what it needs
- SEO Friendly: Clean URLs and proper page structure
- Developer Experience: Easy to understand and maintain
- Performance: Automatic optimization and prefetching
How Next.js Routing Works
Basic Routing Structure
pages/
├── index.js → /
├── about.js → /about
├── contact.js → /contact
├── blog/
│ ├── index.js → /blog
│ └── [slug].js → /blog/[slug]
└── api/
└── users.js → /api/users
1. Static Routes
Simple Pages:
// pages/index.js
export default function Home() {
return <h1>Home Page</h1>
}
// pages/about.js
export default function About() {
return <h1>About Page</h1>
}
// pages/contact.js
export default function Contact() {
return <h1>Contact Page</h1>
}
Nested Routes:
// pages/blog/index.js
export default function Blog() {
return <h1>Blog Posts</h1>
}
// pages/blog/featured.js
export default function Featured() {
return <h1>Featured Posts</h1>
}
2. Dynamic Routes
Single Dynamic Segment:
// pages/blog/[slug].js
import { useRouter } from 'next/router'
export default function BlogPost() {
const router = useRouter()
const { slug } = router.query
return (
<div>
<h1>Blog Post: {slug}</h1>
<p>This is the content for {slug}</p>
</div>
)
}
Multiple Dynamic Segments:
// pages/blog/[year]/[month]/[slug].js
import { useRouter } from 'next/router'
export default function BlogPost() {
const router = useRouter()
const { year, month, slug } = router.query
return (
<div>
<h1>{slug}</h1>
<p>Published: {month}/{year}</p>
</div>
)
}
Catch-all Routes:
// pages/docs/[...slug].js
import { useRouter } from 'next/router'
export default function Docs() {
const router = useRouter()
const { slug } = router.query
return (
<div>
<h1>Documentation</h1>
<p>Path: {slug?.join('/')}</p>
</div>
)
}
3. Optional Catch-all Routes
// pages/shop/[[...slug]].js
import { useRouter } from 'next/router'
export default function Shop() {
const router = useRouter()
const { slug } = router.query
if (slug) {
return <h1>Shop Category: {slug.join('/')}</h1>
}
return <h1>Shop Home</h1>
}
Navigation in Next.js
1. Using the Link Component
Basic Navigation:
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
<Link href="/blog">
<a>Blog</a>
</Link>
</nav>
)
}
Navigation with Styling:
import Link from 'next/link'
import styles from './Navigation.module.css'
export default function Navigation() {
return (
<nav className={styles.nav}>
<Link href="/">
<a className={styles.link}>Home</a>
</Link>
<Link href="/about">
<a className={styles.link}>About</a>
</Link>
</nav>
)
}
External Links:
import Link from 'next/link'
export default function Footer() {
return (
<footer>
<Link href="https://github.com/username">
<a target="_blank" rel="noopener noreferrer">
GitHub
</a>
</Link>
</footer>
)
}
2. Programmatic Navigation
Using useRouter Hook:
import { useRouter } from 'next/router'
import { useState } from 'react'
export default function LoginForm() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const handleLogin = async (credentials) => {
setIsLoading(true)
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
})
if (response.ok) {
router.push('/dashboard')
}
} catch (error) {
console.error('Login failed:', error)
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={handleLogin}>
{/* Form fields */}
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
Navigation Methods:
import { useRouter } from 'next/router'
export default function NavigationExample() {
const router = useRouter()
const handleNavigation = () => {
// Navigate to a new page
router.push('/about')
// Navigate and replace current history entry
router.replace('/login')
// Navigate back
router.back()
// Navigate forward
router.forward()
// Navigate with query parameters
router.push({
pathname: '/blog/[slug]',
query: { slug: 'my-post' }
})
}
return <button onClick={handleNavigation}>Navigate</button>
}
3. Shallow Routing
import { useRouter } from 'next/router'
import { useState } from 'react'
export default function ProductList() {
const router = useRouter()
const [filters, setFilters] = useState({})
const updateFilters = (newFilters) => {
const updatedFilters = { ...filters, ...newFilters }
setFilters(updatedFilters)
// Update URL without triggering a page reload
router.push({
pathname: router.pathname,
query: updatedFilters
}, undefined, { shallow: true })
}
return (
<div>
<FilterComponent onFilterChange={updateFilters} />
<ProductGrid filters={filters} />
</div>
)
}
Advanced Routing Patterns
1. Route Groups
// pages/(marketing)/
// ├── index.js → /
// ├── about.js → /about
// └── contact.js → /contact
// pages/(dashboard)/
// ├── dashboard.js → /dashboard
// └── settings.js → /settings
2. Route Middleware
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// Check authentication
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*']
}
3. Custom 404 and Error Pages
// pages/404.js
export default function Custom404() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
</div>
)
}
// pages/_error.js
function Error({ statusCode }) {
return (
<div>
<h1>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</h1>
</div>
)
}
Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
return { statusCode }
}
export default Error
SEO and Routing
1. Dynamic Meta Tags
// pages/blog/[slug].js
import Head from 'next/head'
import { useRouter } from 'next/router'
export default function BlogPost({ post }) {
const router = useRouter()
const { slug } = router.query
return (
<>
<Head>
<title>{post.title} | My Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:url" content={`https://mysite.com/blog/${slug}`} />
</Head>
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
</>
)
}
2. Canonical URLs
// pages/blog/[slug].js
import Head from 'next/head'
export default function BlogPost({ post }) {
return (
<>
<Head>
<link
rel="canonical"
href={`https://mysite.com/blog/${post.slug}`}
/>
</Head>
{/* Content */}
</>
)
}
Best Practices
1. Route Organization
// ✅ Good: Clear, logical structure
pages/
├── index.js
├── about.js
├── contact.js
├── blog/
│ ├── index.js
│ └── [slug].js
├── products/
│ ├── index.js
│ └── [id].js
└── api/
├── auth.js
└── products.js
2. Link Optimization
// ✅ Good: Proper Link usage
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/about" prefetch={false}>
<a>About</a>
</Link>
<Link href="/blog" prefetch={true}>
<a>Blog</a>
</Link>
</nav>
)
}
3. Error Handling
// ✅ Good: Proper error handling
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
export default function DynamicPage() {
const router = useRouter()
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
if (router.isReady) {
// Handle route parameters
setLoading(false)
}
}, [router.isReady])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>Page content</div>
}
Common Mistakes to Avoid
1. Incorrect Link Usage
// ❌ Wrong: Using <a> without Link
<a href="/about">About</a>
// ✅ Correct: Using Link component
<Link href="/about">
<a>About</a>
</Link>
2. Missing Router Ready Check
// ❌ Wrong: Accessing query before router is ready
const { slug } = router.query
// ✅ Correct: Check if router is ready
if (router.isReady) {
const { slug } = router.query
}
3. Inefficient Navigation
// ❌ Wrong: Using window.location
window.location.href = '/about'
// ✅ Correct: Using Next.js router
router.push('/about')
Summary
Next.js routing provides a powerful and intuitive way to handle navigation in your application:
- File-based routing eliminates configuration overhead
- Dynamic routes handle parameterized URLs
- Link component provides optimized navigation
- useRouter hook enables programmatic navigation
- Automatic code splitting improves performance
Key Takeaways:
- File structure in
pages/
determines routes - Use
Link
component for navigation - Dynamic routes use square brackets
[]
- Check
router.isReady
before accessing query parameters - Leverage shallow routing for URL updates without page reloads
This tutorial is part of the comprehensive Next.js learning path at syscook.dev. Continue to the next chapter to learn about components and layouts.
Author: syscook.dev
Last Updated: December 2024