Next.js

Pages & Routing

25 min Lesson 3 of 40

Introduction to File-Based Routing

One of Next.js's most powerful features is its file-based routing system. Unlike traditional React applications where you need to configure routes manually using libraries like React Router, Next.js automatically creates routes based on the file structure in your pages or app directory.

This approach offers several advantages:

  • No need to configure routing libraries
  • Routes are automatically code-split for optimal performance
  • File structure directly reflects your URL structure
  • Easy to understand and maintain
  • Supports both static and dynamic routes out of the box
Important: Next.js has two routing systems: the Pages Router (traditional, in the pages/ directory) and the App Router (new, in the app/ directory introduced in Next.js 13). This lesson covers both, but focuses primarily on the App Router as it's the recommended approach for new projects.

Pages Router - Basic Routing

Let's start with the traditional Pages Router to understand the fundamentals. With the Pages Router, every file in the pages directory automatically becomes a route.

Creating Basic Pages

Here's how file structure maps to routes:

pages/
├── index.js          → /
├── about.js          → /about
├── contact.js        → /contact
└── blog.js           → /blog

Example of a simple page component:

// pages/about.js
export default function About() {
  return (
    <div>
      <h1>About Us</h1>
      <p>Welcome to our about page!</p>
    </div>
  )
}

That's it! This file is automatically available at http://localhost:3000/about.

Nested Routes with Folders

You can create nested routes using folders:

pages/
├── blog/
│   ├── index.js      → /blog
│   ├── first-post.js → /blog/first-post
│   └── second-post.js → /blog/second-post
└── products/
    ├── index.js      → /products
    └── electronics.js → /products/electronics
// pages/blog/index.js
export default function Blog() {
  return <h1>Blog Home</h1>
}

// pages/blog/first-post.js
export default function FirstPost() {
  return <h1>My First Post</h1>
}

Dynamic Routes

Dynamic routes allow you to create pages with variable segments in the URL. This is essential for content-driven websites where you don't know all possible URLs at build time.

Creating Dynamic Routes

Use square brackets [] to create dynamic route segments:

pages/
└── blog/
    ├── index.js
    └── [slug].js     → /blog/:slug (matches /blog/hello, /blog/world, etc.)
// 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>You are viewing the blog post with slug: {slug}</p>
    </div>
  )
}

Now you can access:

  • /blog/hello-world - slug will be "hello-world"
  • /blog/nextjs-tutorial - slug will be "nextjs-tutorial"
  • /blog/anything - slug will be "anything"
Pro Tip: The name inside the brackets (e.g., [slug]) becomes the name of the query parameter. You can use any name you want: [id], [postId], [username], etc.

Multiple Dynamic Segments

You can have multiple dynamic segments in a single route:

pages/
└── posts/
    └── [category]/
        └── [id].js   → /posts/:category/:id
// pages/posts/[category]/[id].js
import { useRouter } from 'next/router'

export default function Post() {
  const router = useRouter()
  const { category, id } = router.query

  return (
    <div>
      <h1>Category: {category}</h1>
      <p>Post ID: {id}</p>
    </div>
  )
}

This would match URLs like:

  • /posts/technology/123
  • /posts/design/456
  • /posts/marketing/789

Catch-All Routes

Catch-all routes match multiple path segments. They're perfect for building flexible routing structures or documentation sites.

Basic Catch-All Route

Use [...param] syntax to catch all route segments:

pages/
└── docs/
    └── [...slug].js  → /docs/* (matches /docs/a, /docs/a/b, /docs/a/b/c, etc.)
// pages/docs/[...slug].js
import { useRouter } from 'next/router'

export default function Docs() {
  const router = useRouter()
  const { slug } = router.query

  // slug is an array of path segments
  // /docs/getting-started → ['getting-started']
  // /docs/api/users/create → ['api', 'users', 'create']

  return (
    <div>
      <h1>Documentation</h1>
      <p>Path segments: {slug?.join(' / ')}</p>
    </div>
  )
}
Important: The catch-all route does NOT match the base path. For example, [...slug].js in pages/docs/ will NOT match /docs - only /docs/something. Use an index.js file for the base path.

Optional Catch-All Route

To make the catch-all route also match the base path, use double brackets:

pages/
└── docs/
    └── [[...slug]].js  → /docs, /docs/a, /docs/a/b, etc.

Now /docs will also be matched, with slug being undefined.

App Router - The Modern Approach

The App Router, introduced in Next.js 13, brings a more powerful and flexible routing system. It uses the app directory instead of pages.

Key Differences from Pages Router

  • Uses page.js files instead of named files
  • Supports React Server Components by default
  • Includes layouts, loading states, and error boundaries
  • More granular control over rendering strategies

Basic App Router Structure

app/
├── page.js           → /
├── about/
│   └── page.js       → /about
├── blog/
│   ├── page.js       → /blog
│   └── [slug]/
│       └── page.js   → /blog/:slug
└── products/
    └── [id]/
        └── page.js   → /products/:id

Notice that each route is a folder containing a page.js file:

// app/about/page.js
export default function AboutPage() {
  return (
    <div>
      <h1>About Us</h1>
      <p>This is the about page using App Router</p>
    </div>
  )
}

Dynamic Routes in App Router

Dynamic routes work similarly but use folders with bracket notation:

app/
└── blog/
    ├── page.js       → /blog
    └── [slug]/
        └── page.js   → /blog/:slug
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  // In App Router, params are passed as props
  return (
    <div>
      <h1>Blog Post: {params.slug}</h1>
      <p>Reading post with slug: {params.slug}</p>
    </div>
  )
}
Key Difference: In the App Router, dynamic parameters are accessed through the params prop, not through useRouter(). The component also runs on the server by default, making it more performant.

Catch-All Routes in App Router

app/
└── docs/
    ├── page.js               → /docs
    └── [...slug]/
        └── page.js           → /docs/* (multiple segments)
// app/docs/[...slug]/page.js
export default function DocsPage({ params }) {
  const pathSegments = params.slug.join(' → ')

  return (
    <div>
      <h1>Documentation</h1>
      <p>Current path: {pathSegments}</p>
    </div>
  )
}

Navigation Between Routes

Next.js provides the Link component for client-side navigation between routes. This is much faster than traditional page reloads.

Using the Link Component

import Link from 'next/link'

export default function Navigation() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/contact">Contact</Link>
    </nav>
  )
}

Dynamic Links

import Link from 'next/link'

export default function BlogList({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

Programmatic Navigation

Sometimes you need to navigate programmatically (e.g., after form submission):

// Pages Router
import { useRouter } from 'next/router'

export default function LoginForm() {
  const router = useRouter()

  const handleSubmit = async (e) => {
    e.preventDefault()
    // Handle login...
    await router.push('/dashboard')
  }

  return <form onSubmit={handleSubmit}>...</form>
}
// App Router
'use client' // Must be a client component

import { useRouter } from 'next/navigation'

export default function LoginForm() {
  const router = useRouter()

  const handleSubmit = async (e) => {
    e.preventDefault()
    // Handle login...
    router.push('/dashboard')
  }

  return <form onSubmit={handleSubmit}>...</form>
}
Important: Notice the different import paths: next/router for Pages Router vs next/navigation for App Router. In the App Router, you also need to mark components using hooks as client components with the 'use client' directive.

Route Priority and Matching

When you have multiple routes that could match a URL, Next.js follows a specific priority order:

  1. Predefined routes: Static routes like /about
  2. Dynamic routes: Routes like /blog/[slug]
  3. Catch-all routes: Routes like /docs/[...slug]
pages/
├── blog/
│   ├── index.js      → Matches /blog (highest priority)
│   ├── create.js     → Matches /blog/create (second priority)
│   ├── [slug].js     → Matches /blog/anything-else (third priority)
│   └── [...slug].js  → Would never be reached due to [slug].js
Be Careful: A catch-all route will not match if a more specific route exists. Plan your route structure carefully to avoid conflicts.

Query Parameters

You can access query parameters (the part after ? in URLs) in your pages:

// Pages Router - pages/search.js
import { useRouter } from 'next/router'

export default function Search() {
  const router = useRouter()
  const { q, category } = router.query

  // URL: /search?q=nextjs&category=tutorial
  // q = "nextjs"
  // category = "tutorial"

  return (
    <div>
      <h1>Search Results</h1>
      <p>Query: {q}</p>
      <p>Category: {category}</p>
    </div>
  )
}
// App Router - app/search/page.js
export default function SearchPage({ searchParams }) {
  // searchParams are passed as props in App Router
  const { q, category } = searchParams

  return (
    <div>
      <h1>Search Results</h1>
      <p>Query: {q}</p>
      <p>Category: {category}</p>
    </div>
  )
}

404 Custom Error Page

You can create a custom 404 page for when a route doesn't exist:

// pages/404.js (Pages Router)
export default function Custom404() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
      <p>The page you're looking for doesn't exist.</p>
    </div>
  )
}
// app/not-found.js (App Router)
export default function NotFound() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
      <p>The page you're looking for doesn't exist.</p>
    </div>
  )
}
Practice Exercises:
  1. Create a basic blog structure with an index page and at least 3 static blog post pages
  2. Convert your blog to use dynamic routes with a [slug].js file
  3. Create a products section with nested routes: /products/[category]/[id]
  4. Build a documentation site using catch-all routes [...slug]
  5. Add navigation between all your pages using the Link component
  6. Create a search page that reads query parameters from the URL
  7. Implement programmatic navigation in a form that redirects after submission
  8. Test route priority by creating both static and dynamic routes that could conflict

Summary

Next.js's file-based routing system is one of its most powerful features, making it incredibly easy to create and manage routes in your application. We covered:

  • Basic static routes using files and folders
  • Dynamic routes with bracket notation for variable URL segments
  • Nested routes for complex URL structures
  • Catch-all routes for flexible path matching
  • Differences between Pages Router and App Router
  • Navigation with the Link component and programmatic routing
  • Query parameters and custom error pages

Understanding routing is fundamental to building Next.js applications. In the next lesson, we'll explore the differences between the App Router and Pages Router in more depth, including layouts, loading states, and error handling.