Next.js 13 App Router: Complete Migration Guide
A comprehensive guide to migrating from Pages Router to the new App Router in Next.js 13. Learn about the benefits, challenges, and step-by-step migration process.
Next.js 13 introduced the App Router, a fundamental shift in how we build Next.js applications. Built on React Server Components, the App Router offers improved performance, better developer experience, and powerful new features that make building modern web applications easier than ever.
The migration from Pages Router to App Router might seem daunting, but the benefits are substantial: faster page loads, improved SEO, better code organization, and access to cutting-edge React features. In this comprehensive guide, we'll walk through everything you need to know to successfully migrate your Next.js application.
Understanding the App Router Architecture
The App Router represents a paradigm shift in Next.js development. Unlike the Pages Router, which renders everything on the client by default, the App Router uses Server Components as the default, providing better performance and SEO out of the box.
Key Differences Between Pages and App Router
- Server Components by Default: Components are server-rendered unless explicitly marked as client components
- Nested Layouts: Share UI across multiple pages while preserving state
- Streaming: Progressive rendering for faster initial page loads
- Colocation: Keep components, tests, and styles together in the app directory
- Data Fetching: New patterns with async/await directly in components
Step 1: Create the App Directory
Start by creating the app directory alongside your existing pages directory. Both can coexist during migration:
my-app/
├── app/ # New App Router
│ ├── layout.tsx
│ └── page.tsx
├── pages/ # Old Pages Router
│ ├── _app.tsx
│ └── index.tsx
└── public/
Step 2: Create the Root Layout
The root layout replaces _app.tsx and _document.tsx:
// app/layout.tsx
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'My Next.js App',
description: 'Built with App Router',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
Step 3: Migrate Pages to Server Components
Convert your pages to Server Components by default. Server Components can fetch data directly:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // or 'force-cache'
});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function HomePage() {
const data = await getData();
return (
Welcome to Next.js 13
);
}
Step 4: Handle Client-Side Interactivity
For components that need client-side features (hooks, event listeners, browser APIs), use the 'use client' directive:
// app/components/counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
Count: {count}
);
}
Step 5: Migrate Data Fetching
From getServerSideProps
// Before (Pages Router)
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return { props: { data } };
}
// After (App Router)
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // equivalent to getServerSideProps
});
return res.json();
}
export default async function Page() {
const data = await getData();
return {/* render data */};
}
From getStaticProps
// Before (Pages Router)
export async function getStaticProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: { data },
revalidate: 60
};
}
// After (App Router)
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // ISR
});
return res.json();
}
export default async function Page() {
const data = await getData();
return {/* render data */};
}
Step 6: Migrate Dynamic Routes
Dynamic routes work similarly but with a new folder structure:
// Before: pages/blog/[slug].tsx
// After: app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug);
return (
{post.title}
);
}
Step 7: Migrate API Routes
API Routes move to Route Handlers with explicit HTTP methods:
// Before: pages/api/users.ts
export default function handler(req, res) {
if (req.method === 'GET') {
res.status(200).json({ users: [] });
}
}
// After: app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const users = await getUsers();
return NextResponse.json({ users });
}
export async function POST(request: Request) {
const body = await request.json();
const user = await createUser(body);
return NextResponse.json({ user }, { status: 201 });
}
Step 8: Migrate Metadata
Use the new Metadata API instead of next/head:
// app/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Page Title',
description: 'My page description',
openGraph: {
title: 'My Page Title',
description: 'My page description',
images: ['/og-image.jpg'],
},
};
// For dynamic metadata
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
Step 9: Handle Loading and Error States
The App Router provides special files for loading and error states:
// app/loading.tsx
export default function Loading() {
return Loading...;
}
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
Something went wrong!
);
}
Step 10: Implement Streaming
Use Suspense boundaries for progressive rendering:
import { Suspense } from 'react';
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000));
return Slow content loaded!;
}
export default function Page() {
return (
Page Title
Loading slow content... }>
Common Migration Pitfalls
1. Using Hooks in Server Components
Remember: useState, useEffect, and other hooks only work in Client Components.
2. Accessing Request Objects
Use headers() and cookies() from next/headers instead of req.
3. Not Handling Streaming Properly
Wrap async components in Suspense boundaries to enable streaming.
Performance Benefits
After migration, you'll see significant improvements:
- Smaller JavaScript bundles: Server Components don't ship to the client
- Faster initial page loads: Progressive rendering with Streaming
- Better SEO: More content rendered on the server
- Improved caching: Fine-grained control with fetch options
Incremental Migration Strategy
You don't have to migrate everything at once:
- Week 1: Set up app directory and root layout
- Week 2: Migrate simple, static pages
- Week 3: Migrate pages with data fetching
- Week 4: Migrate dynamic routes and API routes
- Week 5: Test thoroughly and optimize
- Week 6: Remove pages directory
Testing Your Migration
Before removing the pages directory:
- Test all routes and ensure they work correctly
- Verify data fetching and caching behavior
- Check that client-side interactions work properly
- Test error handling and loading states
- Validate SEO with
next/seoor metadata - Run lighthouse audits to measure improvements
Conclusion
Migrating to the Next.js 13 App Router is a significant undertaking, but the benefits in performance, developer experience, and future-proofing your application make it worthwhile. The key is to migrate incrementally, test thoroughly, and leverage the new features like Server Components and Streaming to their full potential.
The App Router represents the future of Next.js development, and early adoption gives you access to cutting-edge React features that will become standard in the coming years.
"The App Router isn't just a new routing system—it's a new way of thinking about web development. Embrace Server Components, leverage streaming, and watch your application performance soar." - Vercel Team