Namaste, future full-stack developers! Welcome to this crucial lesson where we’ll demystify one of Next.js’s most important concepts: its routing systems. Next.js, being a powerful React framework, has evolved, and with that evolution came a significant update to how we handle routing. You’ll encounter two primary systems: the App Router and the Pages Router. Understanding both is key to building modern Next.js applications and maintaining existing ones.
Why two routers, you ask? Think of it like this: as web development advances, so do the tools we use. The App Router represents a leap forward, bringing powerful new features like Server Components and nested layouts. However, the Pages Router remains incredibly relevant, especially for legacy projects or when you need to understand existing codebases. In this lesson, we’ll break down both, compare their features, and guide you on when to use each.
The Pages Router was Next.js’s original routing system. It’s based on the pages/ directory and uses a file-system-based routing approach, where each file in pages/ directly maps to a route. It’s intuitive and served the community well for years, introducing concepts like server-side rendering (SSR) and static site generation (SSG) with functions like getStaticProps and getServerSideProps.
The App Router, introduced in Next.js 13, is built on React Server Components and uses the app/ directory. It’s designed to be more flexible, performant, and to leverage the full potential of React’s latest features. It offers new paradigms for data fetching, caching, and nested layouts, aiming to simplify complex application structures and improve user experience.
The Pages Router is straightforward: a file in the pages/ directory becomes a route. This system is still widely used and powers countless Next.js applications.
In the Pages Router, your routes are defined by files within the pages/ directory. For example:
pages/index.tsx maps to /pages/about.tsx maps to /aboutpages/products/index.tsx maps to /productspages/products/[id].tsx maps to /products/:id (a dynamic route)API routes are also defined within this system, inside pages/api/.
Let’s look at a simple product listing and a dynamic product detail page using the Pages Router.
// pages/products/index.tsx
import React from 'react';
import Link from 'next/link';
interface Product {
id: string;
name: string;
}
interface ProductsProps {
products: Product[];
}
export async function getStaticProps() {
// In a real app, you'd fetch this from an API
const products: Product[] = [
{ id: '1', name: 'Laptop' },
{ id: '2', name: 'Smartphone' },
{ id: '3', name: 'Headphones' },
];
return {
props: { products },
};
}
const ProductsPage: React.FC<ProductsProps> = ({ products }) => {
return (
<div>
<h1>Our Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<Link href={`/products/${product.id}`}>{product.name}</Link>
</li>
))}
</ul>
</div>
);
};
export default ProductsPage;
// pages/products/[id].tsx
import React from 'react';
import { useRouter } from 'next/router';
interface Product {
id: string;
name: string;
description: string;
}
interface ProductDetailProps {
product: Product;
}
export async function getStaticPaths() {
const products: Product[] = [
{ id: '1', name: 'Laptop', description: 'Powerful computing' },
{ id: '2', name: 'Smartphone', description: 'Smart communication' },
{ id: '3', name: 'Headphones', description: 'Immersive audio' },
];
const paths = products.map((product) => ({
params: { id: product.id },
}));
return { paths, fallback: false };
}
export async function getStaticProps({ params }: { params: { id: string } }) {
const products: Product[] = [
{ id: '1', name: 'Laptop', description: 'Powerful computing' },
{ id: '2', name: 'Smartphone', description: 'Smart communication' },
{ id: '3', name: 'Headphones', description: 'Immersive audio' },
];
const product = products.find((p) => p.id === params.id);
return { props: { product } };
}
const ProductDetailPage: React.FC<ProductDetailProps> = ({ product }) => {
const router = useRouter();
if (router.isFallback) {
return <div>Loading...</div>;
}
if (!product) {
return <div>Product not found.</div>;
}
return (
<div>
<h1>{product.name}</h1>
<p>ID: {product.id}</p>
<p>Description: {product.description}</p>
<button onClick={() => router.back()}>Go Back</button>
</div>
);
};
export default ProductDetailPage;
The App Router is the future of Next.js. It introduces new paradigms, most notably React Server Components, providing a powerful way to build highly performant and flexible applications.
The App Router operates within the app/ directory. Unlike the Pages Router where each file is a route, in the App Router, folders define routes, and special files within those folders define the UI and behavior.
app/page.tsx maps to / (the root route)app/dashboard/page.tsx maps to /dashboardapp/products/[id]/page.tsx maps to /products/:idThe key idea is that routes are defined by nested folders, and a page.tsx file within a folder is required to make that folder publicly accessible as a route segment.
A fundamental shift with the App Router is the default use of React Server Components. By default, components in the app/ directory are Server Components, which render on the server, reducing client-side JavaScript and improving initial page load performance.
Layouts are another powerful concept. You can define shared UI for a segment and its children using layout.tsx. Layouts are Server Components by default and persist across navigations, maintaining state and avoiding re-renders.
Let’s recreate our product listing with a nested layout using the App Router.
// app/products/layout.tsx
import React from 'react';
import Link from 'next/link';
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<nav style={{ borderBottom: '1px solid #ccc', padding: '10px 0' }}>
<Link href="/">Home</Link> | <Link href="/products">All Products</Link>
</nav>
<main>
{children}
</main>
</div>
);
}
// app/products/page.tsx
import Link from 'next/link';
interface Product {
id: string;
name: string;
}
// This component is a Server Component by default
export default async function ProductsPage() {
// Data fetching directly in Server Components
const products: Product[] = [
{ id: '1', name: 'Laptop' },
{ id: '2', name: 'Smartphone' },
{ id: '3', name: 'Headphones' },
];
return (
<div>
<h1>Our Products (App Router)</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<Link href={`/products/${product.id}`}>{product.name}</Link>
</li>
))}
</ul>
</div>
);
}
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
interface Product {
id: string;
name: string;
description: string;
}
interface ProductDetailPageProps {
params: { id: string };
}
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
const products: Product[] = [
{ id: '1', name: 'Laptop', description: 'Powerful computing' },
{ id: '2', name: 'Smartphone', description: 'Smart communication' },
{ id: '3', name: 'Headphones', description: 'Immersive audio' },
];
const product = products.find((p) => p.id === params.id);
if (!product) {
notFound(); // Use notFound() for 404s in App Router
}
return (
<div>
<h1>{product.name}</h1>
<p>ID: {product.id}</p>
<p>Description: {product.description}</p>
{/* Navigation can be handled with Link or useRouter from 'next/navigation' */}
</div>
);
}
The App Router utilizes several special files to define UI and behavior within a route segment:
page.tsx: Renders the unique UI for a route segment.layout.tsx: Defines shared UI for a segment and its children.loading.tsx: Displays a loading UI while a segment’s content is loading.error.tsx: Catches errors within a route segment and displays a fallback UI.not-found.tsx: Renders a 404 UI when a route is not found within a segment.route.ts (or .js, .mjs, .cjs): Defines Route Handlers, which are custom backend API endpoints, replacing pages/api.template.tsx: Similar to layout.tsx but creates a new instance for each child, allowing state to reset.Let’s summarize the key differences to help solidify your understanding:
| Feature | App Router (app/) | Pages Router (pages/) |
|---|---|---|
| Directory | app/ | pages/ |
| Routing Logic | Folder-based routing, page.tsx defines route UI. | File-based routing, each file in pages/ is a route. |
| Default Component Model | React Server Components (RSC) by default. Use 'use client'; for Client Components. | Traditional React Client Components. |
| Shared Layouts | layout.tsx for nested, persistent layouts. | Custom layouts often implemented with _app.tsx or higher-order components. |
| Data Fetching | fetch() API (extended with caching and revalidation), async/await directly in Server Components, streaming. | getStaticProps, getServerSideProps, getStaticPaths for server-side data fetching. Client-side fetching with useEffect or SWR. |
| Loading UI | loading.tsx file for instant loading states with Suspense. | Typically managed with component state or external libraries. |
| Error Handling | error.tsx file for route-segment error boundaries. | Custom _error.tsx page for application-level errors, or component-level error boundaries. |
| Not Found UI | not-found.tsx file for 404 pages within a segment. Use notFound() function. | Custom 404.tsx page. |
| API Endpoints | route.ts (Route Handlers) within app/ folders. | Files within pages/api/. |
| Client-Side Navigation | next/link and useRouter from next/navigation. | next/link and useRouter from next/router. |
Making the right choice depends on your project’s needs and current stage.
Always prioritize the App Router for new Next.js projects. It’s the recommended and actively developed routing system, offering superior performance, a simpler data fetching model, and better scalability with React Server Components.
If you’re working on an existing Next.js application that uses the pages/ directory, you’ll need to understand and continue using the Pages Router. Migrating a large application can be a significant undertaking, and often, maintaining the existing system is the most practical approach.
Next.js supports coexistence of both routers during a migration. This means you can incrementally move parts of your application from pages/ to app/. This is a powerful feature, but it requires careful planning to avoid route conflicts and ensure correct API usage.
When using both routers in the same application, remember these critical points:
app/ and pages/. If you have app/dashboard/page.tsx, you cannot have pages/dashboard.tsx.useRouter hook from next/router is for the Pages Router, while the one from next/navigation is for the App Router. Do not mix them indiscriminately within the same component or context.getStaticProps/getServerSideProps are exclusive to the Pages Router. In the App Router, you’ll use fetch() or other data fetching libraries directly within Server Components.Let’s get hands-on! Your task is to create a small Next.js project and implement a simple user profile page using both routing systems.
pages/ directory manually).pages/users/[username].tsx.getServerSideProps to fetch user data based on the username parameter. For simplicity, you can mock data (e.g., if username === 'dost', return a user object).app/profile/[username]/page.tsx.async/await) based on the username parameter from params./users/dost and /profile/dost. Observe how the URLs map, and reflect on the differences in data fetching and component structure.This exercise will give you practical experience with both systems and highlight their distinct approaches.
You’ve now got a solid understanding of Next.js’s two powerful routing systems. The Pages Router, with its file-based approach, is a robust system for existing applications. The App Router, leveraging React Server Components and a folder-based structure, is the modern, recommended choice for new projects, offering enhanced performance and flexibility.
Remember, the key is to understand their fundamental differences, especially concerning data fetching, layouts, and the correct usage of routing hooks. By mastering both, you’ll be well-equipped to tackle any Next.js project, whether it’s a brand-new build or maintaining an established application. Keep practicing, and you’ll navigate Next.js routing like a pro! All the best!