AZ

Loading...

Back to Blog
Next.js

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.

Ahmer Zufliqar
January 10, 2024
12 min read
1,920 views
Next.js
App Router
Migration
React
Next.js 13 App Router: Complete Migration Guide

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}
{/* Your footer */}
); }

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:

  1. Week 1: Set up app directory and root layout
  2. Week 2: Migrate simple, static pages
  3. Week 3: Migrate pages with data fetching
  4. Week 4: Migrate dynamic routes and API routes
  5. Week 5: Test thoroughly and optimize
  6. 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/seo or 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

Enjoyed This Article?

Subscribe to get notified when we publish new insights about web development, mobile apps, and digital optimization.