App Router vs Pages Router
Understanding the Two Routing Systems
Next.js currently supports two routing systems: the traditional Pages Router and the new App Router introduced in Next.js 13. Understanding both systems is crucial because you'll encounter legacy projects using Pages Router, while new projects should typically use the App Router for its advanced features and better performance.
Pages Router Overview
The Pages Router has been Next.js's routing system since its inception. It's stable, well-documented, and widely used in production applications.
Pages Router Characteristics
- Location: Routes live in the
pages/directory - File-Based: Each file directly represents a route
- Rendering: Client-side rendering by default, with opt-in SSR and SSG
- Data Fetching: Uses
getServerSideProps,getStaticProps, andgetStaticPaths - Layouts: Implemented manually through the
_app.jsfile
Pages Router Structure Example
pages/
├── _app.js # Custom App component (global layout)
├── _document.js # Custom Document (HTML structure)
├── index.js # Home page (/)
├── about.js # About page (/about)
└── blog/
├── index.js # Blog home (/blog)
└── [slug].js # Dynamic blog post (/blog/:slug)
Pages Router Example
// pages/blog/[slug].js
import { useRouter } from 'next/router'
export default function BlogPost({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
// Server-side rendering
export async function getServerSideProps({ params }) {
const post = await fetchPost(params.slug)
return { props: { post } }
}
// Or static generation
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug)
return { props: { post } }
}
export async function getStaticPaths() {
const posts = await fetchAllPosts()
return {
paths: posts.map(post => ({ params: { slug: post.slug } })),
fallback: false
}
}
App Router Overview
The App Router is the future of Next.js routing. It introduces revolutionary concepts like React Server Components, streaming, and more granular control over loading and error states.
App Router Characteristics
- Location: Routes live in the
app/directory - Folder-Based: Each folder can contain multiple special files (page.js, layout.js, loading.js, etc.)
- Rendering: Server Components by default, with opt-in Client Components
- Data Fetching: Uses native async/await directly in components
- Layouts: First-class support with nested layouts
- Streaming: Built-in support for progressive rendering
App Router Structure Example
app/
├── layout.js # Root layout (wraps all pages)
├── page.js # Home page (/)
├── loading.js # Loading UI for home
├── error.js # Error boundary for home
├── about/
│ └── page.js # About page (/about)
└── blog/
├── layout.js # Blog-specific layout
├── page.js # Blog home (/blog)
├── loading.js # Loading UI for blog
└── [slug]/
├── page.js # Blog post (/blog/:slug)
└── loading.js # Loading UI for post
App Router Example
// app/blog/[slug]/page.js
// This is a Server Component by default
async function getPost(slug) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
cache: 'force-cache' // Equivalent to SSG
// or cache: 'no-store' for SSR
})
return res.json()
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
// Generate static params (like getStaticPaths)
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map(post => ({ slug: post.slug }))
}
Key Architectural Differences
1. Server vs Client Components
Pages Router:
- All components are Client Components by default
- JavaScript for every component is sent to the browser
- Server-side logic only in data fetching functions
App Router:
- All components are Server Components by default
- No JavaScript sent to the client unless needed
- Client Components require the
'use client'directive
// App Router - Server Component (default)
export default function ServerComponent() {
// This runs only on the server
// No JavaScript sent to the client
return <div>I'm a Server Component</div>
}
// App Router - Client Component
'use client'
import { useState } from 'react'
export default function ClientComponent() {
const [count, setCount] = useState(0)
// This runs in the browser
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
2. Data Fetching
Pages Router: Uses special functions that run at build time or request time
// Pages Router - Data fetching
export default function Page({ data }) {
return <div>{data}</div>
}
// Runs on every request (SSR)
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}
// Runs at build time (SSG)
export async function getStaticProps() {
const data = await fetchData()
return { props: { data } }
}
App Router: Uses async components and native fetch with caching options
// App Router - Data fetching
export default async function Page() {
// Fetch directly in the component
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache' // SSG behavior
// cache: 'no-store' // SSR behavior
// next: { revalidate: 3600 } // ISR behavior
}).then(r => r.json())
return <div>{data}</div>
}
3. Layouts
Pages Router: Custom layouts through _app.js or per-page getLayout functions
// pages/_app.js - Global layout
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
// Per-page layout
Page.getLayout = function getLayout(page) {
return (
<DashboardLayout>
{page}
</DashboardLayout>
)
}
App Router: First-class layout support with nesting
// app/layout.js - Root layout (required)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
)
}
// app/dashboard/layout.js - Nested layout
export default function DashboardLayout({ children }) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
)
}
4. Loading States
Pages Router: Manual implementation
// Pages Router - Manual loading state
import { useRouter } from 'next/router'
import { useState, useEffect } from 'react'
export default function Page() {
const router = useRouter()
const [loading, setLoading] = useState(false)
useEffect(() => {
const handleStart = () => setLoading(true)
const handleComplete = () => setLoading(false)
router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeComplete', handleComplete)
return () => {
router.events.off('routeChangeStart', handleStart)
router.events.off('routeChangeComplete', handleComplete)
}
}, [router])
if (loading) return <div>Loading...</div>
return <div>Content</div>
}
App Router: Built-in loading.js file
// app/blog/loading.js - Automatic loading UI
export default function Loading() {
return <div>Loading blog posts...</div>
}
// This automatically shows while page.js is loading
5. Error Handling
Pages Router: Custom error pages (_error.js) or try/catch in components
// pages/_error.js
function Error({ statusCode }) {
return (
<p>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</p>
)
}
Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
return { statusCode }
}
export default Error
App Router: Built-in error.js boundary
// app/blog/error.js - Error boundary
'use client' // Error components must be Client Components
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
Performance Implications
Bundle Size
Pages Router: Every component and its dependencies are bundled and sent to the client, even if they only display static content.
App Router: Server Components never send JavaScript to the client. Only interactive components marked with 'use client' are bundled.
Streaming and Suspense
The App Router supports streaming HTML from the server, allowing faster Time to First Byte and progressive rendering:
// App Router - Streaming with Suspense
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<h1>My Blog</h1>
{/* This renders immediately */}
<Suspense fallback={<div>Loading posts...</div>}>
{/* This streams in when ready */}
<Posts />
</Suspense>
</div>
)
}
async function Posts() {
const posts = await fetchPosts() // Slow operation
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Migration Considerations
When to Use Pages Router
- Maintaining existing applications built with Pages Router
- Projects with specific requirements that aren't yet supported in App Router
- Teams not ready to learn new patterns
When to Use App Router
- All new projects (recommended by Vercel and the Next.js team)
- Applications that need maximum performance
- Projects that benefit from Server Components (reduced JavaScript)
- Applications requiring advanced features like streaming and suspense
Gradual Migration Strategy
You can migrate gradually by having both routers in the same project:
project/
├── app/ # New routes using App Router
│ └── dashboard/
│ └── page.js # /dashboard
└── pages/ # Legacy routes using Pages Router
└── blog/
└── [slug].js # /blog/:slug
app/about/page.js and pages/about.js, only the App Router version will be accessible.
Comparison Table
Feature | Pages Router | App Router ---------------------|---------------------|-------------------- Directory | pages/ | app/ Default Component | Client Component | Server Component Data Fetching | Special functions | async/await in component Layouts | Manual via _app | Built-in nested layouts Loading States | Manual | loading.js file Error Handling | _error.js | error.js boundary Streaming | Not supported | Built-in support Route Files | Named files | Special files in folders Performance | Good | Better (less JS) Learning Curve | Easier | Steeper initially Stability | Very stable | Stable (13.4+)
- Create the same page using both Pages Router and App Router to understand the differences
- Build a blog post page with loading and error states in the App Router
- Compare bundle sizes: Create identical pages with both routers and check the JavaScript bundle size in the Network tab
- Implement nested layouts in App Router (e.g., root layout → dashboard layout → specific page)
- Convert a Pages Router component to App Router, identifying which parts should be Server Components vs Client Components
- Create a streaming example using Suspense in the App Router
- Experiment with different caching strategies (force-cache, no-store, revalidate) in App Router data fetching
Summary
The App Router represents a significant evolution in Next.js architecture, bringing powerful new features while maintaining the simplicity that made Next.js popular. While the Pages Router remains stable and supported, the App Router is the recommended choice for new projects due to its superior performance, better developer experience with features like built-in loading and error states, and future-proof architecture with React Server Components.
Key takeaways:
- App Router uses Server Components by default, reducing JavaScript sent to the client
- Data fetching is more intuitive with async/await directly in components
- Layouts, loading, and error states have first-class support
- Both routers can coexist during migration
- Choose App Router for new projects; maintain Pages Router for existing apps
In the next lesson, we'll dive deep into layouts and templates in the App Router, exploring how to create reusable UI structures and optimize your application's architecture.