Next.js has two routing systems: the Pages Router (legacy) and the App Router (modern). If you're learning Next.js now, you need to understand both—not because you'll use both, but because you'll encounter Pages Router code in existing projects and tutorials. This lesson will clarify the differences and confirm why we're focusing exclusively on the App Router in this series.
The Two Routing Systems
Pages Router (Legacy)
- Released: Next.js 1.0 (2016)
- Directory:
pages/ - Status: Still supported, not deprecated
- Use case: Existing projects, legacy code
- Rendering: Client components by default
- Data fetching: getServerSideProps, getStaticProps
App Router (Modern)
- Released: Next.js 13 (2022), stable in 13.4
- Directory:
app/ - Status: Recommended for all new projects
- Use case: New projects, modern features
- Rendering: Server components by default
- Data fetching: Direct async/await in components
Clear Recommendation
For any new project starting today, use the App Router. The Pages Router isn't going away, but the App Router is where all new features and improvements are focused. This entire tutorial series teaches the App Router exclusively.
Why Does Next.js Have Two Routing Systems?
Understanding the history helps explain why both exist:
The Journey
- 2016-2022: Next.js used only the Pages Router. It was revolutionary at the time and powered thousands of production applications.
- 2022: React announced Server Components—a new way to render React on the server with better performance.
- October 2022: Next.js 13 introduced the App Router as an experimental feature to support React Server Components.
- May 2023: Next.js 13.4 marked the App Router as stable and production-ready.
- Today: Both routers coexist. Pages Router remains fully supported for existing projects, while App Router is recommended for new ones.
🔄 Incremental Adoption
Next.js deliberately designed both routers to work side-by-side. This lets teams migrate from Pages Router to App Router incrementally, route by route, without rewriting their entire application at once.
Key Differences: Side-by-Side Comparison
1. File Structure and Routing
Pages Router
pages/
index.tsx → /
about.tsx → /about
blog/
index.tsx → /blog
[slug].tsx → /blog/:slug
api/
users.ts → /api/usersEach file is a route. Simple and straightforward.
App Router
app/
page.tsx → /
about/
page.tsx → /about
blog/
page.tsx → /blog
[slug]/
page.tsx → /blog/:slug
api/
users/
route.ts → /api/usersFolders define routes, special files define behavior.
2. Component Types
Pages Router
All components are Client Components by default. Everything runs in the browser unless you use special data fetching functions.
// pages/index.tsx - Client Component
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
// This runs in the browser
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}App Router
All components are Server Components by default. They run on the server unless you add "use client".
// app/page.tsx - Server Component (default)
export default async function Home() {
// This runs on the SERVER
const data = await fetch('https://api.example.com/data');
const posts = await data.json();
return <div>{posts.map(post => ...)}</div>;
}
// To make it a Client Component:
"use client"; // Add this at the top
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Server vs Client Components - Quick Recap
| Feature | Server Component | Client Component |
|---|---|---|
| Runs on | Server (Node.js) | Browser |
| Can use async/await | ✓ Yes | ✓ Yes |
| Can fetch data directly | ✓ Yes | Via API calls |
| Access to databases | ✓ Yes | ✗ No |
| Can use environment variables | All variables | NEXT_PUBLIC_ only |
| Can use useState/useEffect | ✗ No | ✓ Yes |
| Can use event handlers | ✗ No | ✓ Yes |
| Can access browser APIs | ✗ No | ✓ Yes |
| Bundle size impact | No impact | Adds to bundle |
| SEO friendly | ✓ Yes | Depends |
3. Data Fetching
Pages Router - Special Functions
Use special functions that run at build time or request time:
import { GetServerSideProps } from 'next';
interface Post {
title: string;
content: string;
}
export default function BlogPost({ post }: { post: Post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// This function runs on the server for each request
export const getServerSideProps: GetServerSideProps = async (context) => {
const { slug } = context.params!;
const res = await fetch(`https://api.example.com/posts/${slug}`);
const post = await res.json();
return {
props: { post },
};
};App Router - Direct Async Components
Just use async/await directly in your Server Components:
// No imports needed! Just make the component async
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
// Fetch directly in the component!
const res = await fetch(`https://api.example.com/posts/${params.slug}`);
const post = await res.json();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Pages Router Data Flow
How data flows from server to client
1. Component Renders
Page component loads in the browser
2. getServerSideProps/getStaticProps
Special functions run on server (if defined)
3. Data Passed as Props
Data returned as props to component
4. Component Re-renders
Component renders with data on client
App Router Data Flow
How data flows from server to client
1. Server Component Executes
Component runs on server by default
2. Direct Data Fetching
Fetch data directly with async/await
3. HTML Generated
Server generates complete HTML
4. HTML Sent to Browser
Fully rendered content delivered instantly
⚡ App Router Advantage
Notice how much cleaner the App Router code is? No special functions to remember, no prop passing between data fetching and rendering. Just async/await where you need it!
4. Layouts and Shared UI
Pages Router - _app.tsx and _document.tsx
Use special files to wrap all pages:
import type { AppProps } from 'next/app';
import '../styles/globals.css';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<nav>Navigation</nav>
<Component {...pageProps} />
<footer>Footer</footer>
</>
);
}Problem: This wraps every page. You can't have different layouts for different sections easily.
App Router - Nested Layouts
Create layouts at any level:
// Root layout (wraps everything)
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<nav>Navigation</nav>
{children}
<footer>Footer</footer>
</body>
</html>
);
}// Dashboard-specific layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-wrapper">
<aside>Dashboard Sidebar</aside>
<main>{children}</main>
</div>
);
}Advantage: Different sections can have completely different layouts!
5. Loading and Error States
Pages Router - Manual Implementation
import { useState, useEffect } from 'react';
export default function Dashboard() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/dashboard')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{/* Render data */}</div>;
}App Router - Automatic with Special Files
// Automatically shows while page loads
export default function Loading() {
return <div>Loading dashboard...</div>;
}'use client';
// Automatically shows on error
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}// Just focus on the happy path!
export default async function Dashboard() {
const data = await fetch('/api/dashboard').then(r => r.json());
return <div>{/* Render data */}</div>;
}6. API Routes
Pages Router
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'GET') {
res.status(200).json({ users: [] });
} else if (req.method === 'POST') {
res.status(201).json({ user: req.body });
}
}App Router
// Export named functions for each method
export async function GET() {
return Response.json({ users: [] });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json(
{ user: body },
{ status: 201 }
);
}Both approaches work fine. The App Router version uses the Web Standards Request/Response API.
Complete Feature Comparison
| Feature | Pages Router | App Router |
|---|---|---|
| Server Components | ❌ No | ✅ Yes (default) |
| Nested Layouts | ❌ Limited | ✅ Full support |
| Loading UI | Manual | Built-in (loading.tsx) |
| Error Boundaries | Manual | Built-in (error.tsx) |
| Data Fetching | Special functions | Async components |
| Streaming | ❌ No | ✅ Yes (Suspense) |
| Server Actions | ❌ No | ✅ Yes |
| Parallel Routes | ❌ No | ✅ Yes |
| Intercepting Routes | ❌ No | ✅ Yes |
| Learning Curve | Easier initially | Steeper but worth it |
| Future Support | Maintained, no new features | Active development |
| Recommendation | Existing projects only | ✅ All new projects |
Why the App Router is Better
Let's be clear about the advantages that make App Router the recommended choice:
1. Better Performance Out of the Box
- Smaller Client Bundles: Server Components don't ship JavaScript to the browser
- Faster Initial Load: HTML is generated on the server
- Automatic Code Splitting: Only load what's needed for each route
- Streaming: Send parts of the page as they're ready
2. Better Developer Experience
- Simpler Data Fetching: No special functions, just async/await
- Better File Organization: Group related files together
- Built-in Features: Loading and error states without extra code
- Flexible Layouts: Different layouts for different sections
3. Modern React Features
- Server Components: Access to React's latest innovations
- Server Actions: Handle mutations without API routes
- Streaming: Progressive rendering with Suspense
- Improved Caching: Better control over data freshness
4. Future-Proof
- All new Next.js features target the App Router first
- React team is focusing on Server Components
- Community and ecosystem moving to App Router
- Better prepared for future web standards
The Bottom Line
The App Router isn't just "new"—it's better. It provides better performance, better developer experience, and access to modern React features that aren't available in the Pages Router.
Can You Mix Both? Migration Strategy
Yes, you can use both routers in the same project! This is primarily useful for migration:
my-next-app/
app/ ← App Router (new routes)
page.tsx → /
dashboard/
page.tsx → /dashboard
pages/ ← Pages Router (old routes)
api/
legacy.ts → /api/legacy
old-page.tsx → /old-pageMigration Path
- Create app directory: Add it alongside pages/
- Migrate incrementally: Move routes one by one to app/
- Test thoroughly: Ensure each migrated route works
- Remove pages directory: When all routes are migrated
Priority Rules
If the same route exists in both routers:
- App Router takes priority for page routes
app/about/page.tsxoverridespages/about.tsx- API routes in
pages/apistill work even withapp/directory
🎯 For New Projects
Skip the Pages Router entirely! There's no reason to start with Pages Router in 2024. Begin with App Router from day one and enjoy all the modern features.
Common Questions
Is the Pages Router Being Deprecated?
No. The Pages Router is not deprecated and won't be removed. It's still fully supported and maintained. However, new features are being built for the App Router, so that's where the ecosystem is moving.
Will My Pages Router App Stop Working?
No. Existing Pages Router applications will continue to work indefinitely. Next.js is committed to supporting it for the long term.
Should I Migrate My Existing Pages Router App?
It depends:
- If it's working fine: No rush to migrate
- If you need new features: Consider migrating affected routes
- If starting new sections: Use App Router for new code
- If it's a greenfield project: Definitely use App Router
Why Are There Still Pages Router Tutorials?
Several reasons:
- Many existing applications use Pages Router
- Some tutorials haven't been updated yet
- The Pages Router is still valid for certain use cases
- Developers need to maintain legacy code
Will Learning App Router Help Me With Pages Router?
Yes! The concepts transfer. If you understand App Router, you'll understand Pages Router easily. The reverse is also true, but learning App Router first gives you access to more modern patterns.
Which Tutorials Should You Follow?
✅ Look for These Indicators (App Router)
- Mentions "App Router" or "Next.js 13+"
- Shows code in
app/directory - Uses
page.tsxandlayout.tsx - Talks about Server Components
- Published after May 2023
⚠️ These Indicate Pages Router (Legacy)
- Shows code in
pages/directory - Uses
_app.tsxand_document.tsx - Mentions
getServerSidePropsorgetStaticProps - Talks about "Next.js 12" or earlier versions
- Published before 2023
📚 This Tutorial Series
Every lesson in this series teaches the App Router exclusively. You won't see any Pages Router code here. We're focused on teaching you the modern, recommended approach from the start.
Real-World Comparison: A Blog Post Page
Let's see how you'd build the same feature in both routers:
The Requirement
Display a blog post with data from an API, with loading and error states, in a custom layout.
Pages Router Implementation
import { GetServerSideProps } from 'next';
import { useState, useEffect } from 'react';
import BlogLayout from '@/components/BlogLayout';
interface Post {
title: string;
content: string;
author: string;
}
export default function BlogPost({ slug }: { slug: string }) {
const [post, setPost] = useState<Post | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(`/api/posts/${slug}`)
.then(res => res.json())
.then(data => {
setPost(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [slug]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!post) return <div>Post not found</div>;
return (
<BlogLayout>
<article>
<h1>{post.title}</h1>
<p>By {post.author}</p>
<div>{post.content}</div>
</article>
</BlogLayout>
);
}
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
return {
props: {
slug: params?.slug,
},
};
};App Router Implementation
Split across multiple files:
// Blog layout (wraps all blog pages)
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="blog-layout">
<aside>Blog Sidebar</aside>
<main>{children}</main>
</div>
);
}// Automatic loading UI
export default function Loading() {
return <div>Loading post...</div>;
}'use client';
// Automatic error UI
export default function Error({
error,
}: {
error: Error;
}) {
return <div>Error: {error.message}</div>;
}// The actual page - clean and focused!
interface Post {
title: string;
content: string;
author: string;
}
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
// Direct data fetching
const res = await fetch(`https://api.example.com/posts/${params.slug}`);
if (!res.ok) {
throw new Error('Failed to fetch post');
}
const post: Post = await res.json();
return (
<article>
<h1>{post.title}</h1>
<p>By {post.author}</p>
<div>{post.content}</div>
</article>
);
}Notice the differences:
- App Router: Separation of concerns (layout, loading, error, page)
- App Router: No useState or useEffect for data fetching
- App Router: Cleaner, more focused page component
- App Router: Automatic handling of loading and error states
- Pages Router: Everything in one file, more boilerplate
Key Takeaways
- Next.js has two routing systems: Pages Router (legacy) and App Router (modern)
- App Router is recommended for all new projects
- Pages Router is not deprecated but isn't getting new features
- App Router uses Server Components by default, Pages Router uses Client Components
- App Router has better performance, DX, and access to modern React features
- You can use both routers during migration
- This tutorial series focuses exclusively on the App Router
- Learning App Router first makes understanding Pages Router easier
- Look for "App Router" or "Next.js 13+" in tutorials to ensure you're learning the modern approach
What's Next?
Now that you understand why we're using the App Router, it's time to dive deep into file-based routing—the core concept that makes Next.js so powerful and intuitive.
In the next lesson, we'll explore how folders and files in the app directory automatically create routes, how to create static and dynamic routes, and how Next.js's routing conventions make building complex applications simple.
🎓 You're Learning the Right Way
By focusing on the App Router from the start, you're learning Next.js the modern way. You're building skills that will serve you for years to come, and you're positioned to take advantage of all the latest React and Next.js innovations!