Back to Blog
August 19, 20269 min read2 views
Next.js App Router vs Pages Router: When and Why to Migrate
Next.jsTypeScriptWeb Development
# Next.js App Router vs Pages Router: When and Why to Migrate
## Introduction
If you're working with Next.js, you've probably heard the debate: Pages Router vs App Router. One isn't universally better than the other — the right choice depends on your project's needs, your team's experience, and what you're building.
After migrating several production applications from Pages Router to App Router, I've developed a practical framework for deciding when to migrate and how to do it successfully.
## Understanding the Core Differences
Before diving into migration decisions, let's clarify what each router actually does differently.
### Pages Router (Legacy)
The Pages Router uses a file-based routing system where each file in the `pages` directory becomes a route. It's been the standard since Next.js 9 and powers millions of production applications.
**Key characteristics:**
- `getStaticProps` and `getServerSideProps` for data fetching
- API routes in `pages/api`
- Client-side rendering by default
- `_app.tsx` and `_document.tsx` for global customization
### App Router (Modern)
The App Router, introduced in Next.js 13, brings React Server Components, nested layouts, and a new data fetching model. It's the future of Next.js development.
**Key characteristics:**
- React Server Components by default
- `layout.tsx` for shared layouts
- Server Actions for mutations
- `loading.tsx` and `error.tsx` for UI states
- Streaming and Suspense support
## When to Stay with Pages Router
Not every project needs to migrate. Here's when I recommend sticking with Pages Router:
### 1. Your Application is Stable and Working
If your Pages Router application is performing well, meeting business requirements, and your team is productive with it, migration may not be necessary. The old saying applies: "If it ain't broke, don't fix it."
### 2. You're Using Pages Router-Specific Patterns Heavily
Some patterns work differently in App Router:
- Custom `_app.tsx` with complex logic
- Custom `_document.tsx` with specific optimizations
- Heavy use of `getStaticProps` with `revalidate`
- Complex API routes with middleware
If you've built sophisticated patterns around these features, migration requires careful planning.
### 3. Your Team Isn't Ready
App Router requires understanding React Server Components, a fundamentally different mental model. If your team is under pressure to deliver features, adding migration complexity may not be wise.
### 4. You're Using Libraries That Don't Support App Router
Some popular libraries haven't fully embraced App Router yet. Check your dependencies before migrating.
## When to Migrate to App Router
Here's when migration makes sense:
### 1. You Need Better Performance
App Router's Server Components and streaming capabilities can significantly improve performance:
- **Reduced JavaScript bundle size** — Server Components don't ship JavaScript to the client
- **Faster initial page loads** — Streaming allows progressive rendering
- **Better Core Web Vitals** — Improved LCP, FID, and CLS scores
### 2. You're Building New Features That Benefit from Server Components
If you're adding features that would benefit from server-side rendering (dashboards, content-heavy pages, e-commerce product pages), App Router provides a cleaner architecture.
### 3. You Want Nested Layouts
App Router's nested layout system is more powerful than Pages Router's `_app.tsx`:
```tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
{children}
)
}
```
This pattern is cleaner and more maintainable than wrapping everything in `_app.tsx`.
### 4. You Need Server Actions
Server Actions simplify form handling and mutations:
```tsx
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')
// Save to database
revalidatePath('/posts')
}
```
This eliminates boilerplate API routes for simple operations.
## Migration Strategy: The Incremental Approach
I don't recommend big-bang migrations. Instead, use the incremental approach:
### Phase 1: Preparation
1. **Audit your current application**
- List all pages and their data fetching patterns
- Identify shared components and layouts
- Check library compatibility
2. **Set up the App Router alongside Pages Router**
Next.js supports both routers simultaneously. You can have:
```
/pages (existing routes)
/app (new routes)
```
3. **Create a migration checklist**
For each page, document:
- Current data fetching method
- Dependencies on `_app.tsx` or `_document.tsx`
- Client-side state management
- API routes used
### Phase 2: Start with Simple Pages
Begin with pages that have minimal complexity:
1. **Static pages** (about, contact, terms)
2. **Pages with simple data fetching**
3. **Pages without complex client-side logic**
For each page:
1. Create the new route in `app/`
2. Migrate the data fetching to Server Components
3. Test thoroughly
4. Update internal links
### Phase 3: Migrate Complex Pages
Once you're comfortable with simple migrations, tackle complex pages:
1. **Pages with authentication**
2. **Pages with real-time updates**
3. **Pages with complex forms**
4. **Pages with heavy client-side interactivity**
### Phase 4: Migrate API Routes
Migrate API routes to Server Actions where appropriate:
- **Simple CRUD operations** → Server Actions
- **Webhooks and external integrations** → Route Handlers
- **Complex APIs** → Consider keeping as Route Handlers
## Real-World Migration Example
Let me walk through migrating a typical Next.js page.
### Before (Pages Router)
```tsx
// pages/dashboard.tsx
import { GetServerSideProps } from 'next'
import { useState, useEffect } from 'react'
interface DashboardProps {
user: User
stats: Stats
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
if (!session) return { redirect: { destination: '/login', permanent: false } }
const user = await getUser(session.userId)
const stats = await getStats(session.userId)
return { props: { user, stats } }
}
export default function Dashboard({ user, stats }: DashboardProps) {
const [realTimeStats, setRealTimeStats] = useState(stats)
useEffect(() => {
const interval = setInterval(async () => {
const updated = await fetch('/api/stats').then(r => r.json())
setRealTimeStats(updated)
}, 5000)
return () => clearInterval(interval)
}, [])
return (
)
}
```
### After (App Router)
```tsx
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { Suspense } from 'react'
import { getUser, getStats } from '@/lib/data'
import { StatsWidget } from './stats-widget'
export default async function DashboardPage() {
const session = await getSession()
if (!session) redirect('/login')
const [user, stats] = await Promise.all([
getUser(session.userId),
getStats(session.userId)
])
return (
}>
)
}
```
```tsx
// app/dashboard/stats-widget.tsx
'use client'
import { useState, useEffect } from 'react'
interface StatsWidgetProps {
userId: string
initialStats: Stats
}
export function StatsWidget({ userId, initialStats }: StatsWidgetProps) {
const [stats, setStats] = useState(initialStats)
useEffect(() => {
const interval = setInterval(async () => {
const updated = await fetch(`/api/stats?userId=${userId}`).then(r => r.json())
setStats(updated)
}, 5000)
return () => clearInterval(interval)
}, [userId])
return (
}>
}>
)
}
```
### Caching
Understand Next.js caching layers:
1. **Request Memoization** — Deduplicates requests in a single render
2. **Data Cache** — Persists fetched data across requests
3. **Full Route Cache** — Caches the rendered result of a route
4. **Router Cache** — Client-side cache for pre-fetched routes
## Decision Framework
Use this checklist to decide whether to migrate:
**Stay with Pages Router if:**
- [ ] Application is stable and meeting requirements
- [ ] Team is productive and not asking for App Router features
- [ ] Heavy use of Pages Router-specific patterns
- [ ] Dependencies don't fully support App Router
- [ ] No clear performance issues
**Migrate to App Router if:**
- [ ] Need better performance (Core Web Vitals)
- [ ] Building new features that benefit from Server Components
- [ ] Want nested layouts
- [ ] Need Server Actions for form handling
- [ ] Team is ready to learn the new patterns
- [ ] Dependencies support App Router
## Conclusion
The Pages Router vs App Router decision isn't about which is "better" — it's about which is right for your specific situation. Both are production-ready and will continue to be supported.
If you're starting a new project, App Router is the way to go. If you have an existing Pages Router application that's working well, there's no rush to migrate. If you need better performance or are building features that benefit from Server Components, migration is worth the investment.
The key is to migrate incrementally, test thoroughly, and focus on the benefits that matter most to your users and your team.
---
**Need help with your Next.js migration?** [Contact me](/contact) to discuss your project and how I can help.
Welcome, {user.name}
Welcome, {user.name}
{/* Stats display */}
)
}
```
**Key improvements:**
- Data fetching moved to the page level (Server Component)
- Client component isolated for real-time updates
- Better separation of concerns
- Cleaner async/await patterns
## Common Migration Pitfalls
### 1. Mixing Server and Client Components Incorrectly
**Mistake:** Making everything a Client Component.
**Solution:** Start with Server Components by default. Only add `'use client'` when you need:
- `useState`, `useEffect`, or other React hooks
- Event handlers
- Browser APIs
- Third-party libraries that require client-side rendering
### 2. Forgetting About Hydration
**Mistake:** Using server-only code in Client Components.
**Solution:** Remember the boundary:
- Server Components can access databases, file system, etc.
- Client Components can only access what's available in the browser
### 3. Not Updating Data Fetching Patterns
**Mistake:** Still using `getServerSideProps` in App Router.
**Solution:** Use the new data fetching patterns:
```tsx
// Server Component
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Cache for 1 hour
})
return res.json()
}
```
### 4. Ignoring Layout Nesting
**Mistake:** Creating separate layouts for every page.
**Solution:** Use nested layouts strategically:
- Root layout for global elements (navbar, footer)
- Section layouts for shared UI (dashboard sidebar)
- Page layouts only when absolutely necessary
## Performance Considerations
### Bundle Size
App Router can significantly reduce bundle size:
```tsx
// Server Component - no JavaScript sent to client
async function ProductList() {
const products = await db.product.findMany()
return (
-
{products.map(product => (
- {product.name} ))}
Dashboard
Comments (0)
Loading comments...