Pages & Routing
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
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"
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>
)
}
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.jsfiles 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>
)
}
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>
}
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:
- Predefined routes: Static routes like
/about - Dynamic routes: Routes like
/blog/[slug] - 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
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>
)
}
- Create a basic blog structure with an index page and at least 3 static blog post pages
- Convert your blog to use dynamic routes with a [slug].js file
- Create a products section with nested routes: /products/[category]/[id]
- Build a documentation site using catch-all routes [...slug]
- Add navigation between all your pages using the Link component
- Create a search page that reads query parameters from the URL
- Implement programmatic navigation in a form that redirects after submission
- 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.