Skip to main content

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>
}

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
// ✅ 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

// ❌ 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