osmoto.
Case StudiesBlogBook Consultation

Services

Stripe IntegrationSubscription BillingPayment Automation & AINext.js OptimizationAudit & Fix

Solutions

For FoundersFor SaaS CompaniesFor E-Commerce StoresFor Marketplaces

Resources

Implementation GuideWebhook Best PracticesPCI Compliance GuideStripe vs Alternatives
Case StudiesBlog
Book Consultation
osmoto.

Professional Stripe integration services

Services

  • Stripe Integration
  • Subscription Billing
  • E-Commerce Integration
  • Next.js Optimization
  • Audit & Fix

Solutions

  • For Founders
  • For SaaS
  • For E-Commerce
  • For Marketplaces
  • Integration as a Service

Resources

  • Implementation Guide
  • Webhook Guide
  • PCI Compliance
  • Stripe vs Alternatives

Company

  • About
  • Case Studies
  • Process
  • Pricing
  • Contact
© 2026 Osmoto · Professional Stripe Integration Services
Back to Blog
Next.js13 min read

Migrating from Next.js Pages Router to App Router: A Step-by-Step Guide

The Next.js App Router represents a fundamental shift in how we structure React applications, moving from file-based routing to a more flexible, component-drive...

Osmoto Team

Senior Software Engineer

January 14, 2026
Migrating from Next.js Pages Router to App Router: A Step-by-Step Guide

The Next.js App Router represents a fundamental shift in how we structure React applications, moving from file-based routing to a more flexible, component-driven approach. While the migration from Pages Router to App Router might seem straightforward on the surface, the reality involves navigating significant architectural changes, handling breaking changes in data fetching patterns, and restructuring your entire application layout system.

Many development teams delay this migration due to concerns about breaking existing functionality, especially in production applications with complex routing structures, authentication flows, and API integrations. However, the benefits—including improved performance through React Server Components, better developer experience with colocation, and enhanced SEO capabilities—make this migration essential for long-term maintainability and scalability.

This guide walks through the complete migration process, covering everything from initial project setup to handling edge cases that commonly trip up developers during the transition.

Understanding the Architectural Differences

Before diving into the migration steps, it's crucial to understand how App Router fundamentally differs from Pages Router. The Pages Router uses a file-based system where each file in the pages directory automatically becomes a route. App Router, introduced in Next.js 13, uses a folder-based system where each folder can contain multiple files with specific naming conventions.

Key Structural Changes

In Pages Router, your structure might look like:

pages/
  index.js          // Route: /
  about.js          // Route: /about
  blog/
    index.js        // Route: /blog
    [slug].js       // Route: /blog/[slug]
  api/
    users.js        // API Route: /api/users

App Router transforms this into:

app/
  page.js           // Route: /
  about/
    page.js         // Route: /about
  blog/
    page.js         // Route: /blog
    [slug]/
      page.js       // Route: /blog/[slug]
  api/
    users/
      route.js      // API Route: /api/users

The most significant change is that App Router requires explicit page.js files to create accessible routes. This allows you to colocate components, styles, and tests within the same directory without accidentally creating routes.

Data Fetching Philosophy Shift

Pages Router relies heavily on special functions like getServerSideProps, getStaticProps, and getInitialProps. App Router embraces React Server Components, allowing you to fetch data directly in your components using standard async/await syntax.

// Pages Router approach export async function getServerSideProps(context) { const res = await fetch(`https://api.example.com/posts/${context.params.id}`) const post = await res.json() return { props: { post } } } export default function Post({ post }) { return <div>{post.title}</div> }
// App Router approach async function Post({ params }) { const res = await fetch(`https://api.example.com/posts/${params.id}`) const post = await res.json() return <div>{post.title}</div> }

Pre-Migration Assessment and Planning

Inventory Your Current Structure

Start by creating a comprehensive inventory of your existing Pages Router structure. Document:

  1. All routes and their corresponding files
  2. Data fetching patterns used (getServerSideProps, getStaticProps, getInitialProps)
  3. Custom App and Document components (_app.js, _document.js)
  4. API routes and their functionality
  5. Middleware implementations
  6. Custom error pages (_error.js, 404.js)

Create a migration checklist that maps each Pages Router file to its App Router equivalent. This prevents overlooking critical functionality during the transition.

Identify Breaking Changes

Several patterns from Pages Router don't have direct equivalents in App Router:

  • getInitialProps: No direct equivalent; requires restructuring to use Server Components or client-side fetching
  • Custom _app.js logic: Must be moved to layout components or providers
  • _document.js customizations: Handled through layout.js files and metadata API
  • Automatic static optimization: Replaced with explicit static/dynamic rendering decisions

Version Compatibility Check

Ensure you're running Next.js 13.4 or later for stable App Router support. Check your dependencies for compatibility:

npx next@latest npm list react react-dom

Some third-party packages may not be compatible with React Server Components. Identify these early and plan alternatives or client-side implementations.

Step-by-Step Migration Process

Step 1: Enable App Router

First, create the app directory alongside your existing pages directory. Next.js supports running both routers simultaneously during migration.

mkdir app

Update your next.config.js to explicitly enable the experimental features if you're on an older version:

/** @type {import('next').NextConfig} */ const nextConfig = { experimental: { appDir: true, // Only needed for Next.js < 13.4 }, } module.exports = nextConfig

Step 2: Create Root Layout

Every App Router application requires a root layout. Create app/layout.js:

import './globals.css' export const metadata = { title: 'Your App Name', description: 'Your app description', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }

This replaces the functionality of _document.js and provides the basic HTML structure for your application.

Step 3: Migrate Your Homepage

Start with your main pages/index.js file. Create app/page.js:

// If your original page had getServerSideProps async function getData() { const res = await fetch('https://api.example.com/data') if (!res.ok) { throw new Error('Failed to fetch data') } return res.json() } export default async function HomePage() { const data = await getData() return ( <main> <h1>Welcome</h1> {/* Your existing JSX */} </main> ) }

Step 4: Handle Global State and Providers

If your _app.js included providers (Redux, Theme, Auth), create a providers component:

// app/providers.tsx 'use client' import { ThemeProvider } from 'next-themes' import { AuthProvider } from '@/lib/auth' export function Providers({ children }: { children: React.ReactNode }) { return ( <ThemeProvider> <AuthProvider> {children} </AuthProvider> </ThemeProvider> ) }

Then wrap your root layout:

// app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers> {children} </Providers> </body> </html> ) }

Step 5: Migrate Dynamic Routes

Dynamic routes require careful attention to parameter handling. Convert pages/blog/[slug].js:

// app/blog/[slug]/page.tsx interface Props { params: { slug: string } } async function getPost(slug: string) { const res = await fetch(`https://api.example.com/posts/${slug}`) if (!res.ok) { throw new Error('Failed to fetch post') } return res.json() } export default async function BlogPost({ params }: Props) { const post = await getPost(params.slug) return ( <article> <h1>{post.title}</h1> <div>{post.content}</div> </article> ) } // Generate metadata dynamically export async function generateMetadata({ params }: Props) { const post = await getPost(params.slug) return { title: post.title, description: post.excerpt, } }

Step 6: Convert API Routes

API routes follow a similar folder structure but use route.js files:

// app/api/users/route.ts import { NextRequest, NextResponse } from 'next/server' export async function GET(request: NextRequest) { const users = await fetchUsers() return NextResponse.json(users) } export async function POST(request: NextRequest) { const body = await request.json() const user = await createUser(body) return NextResponse.json(user, { status: 201 }) }

For dynamic API routes, the folder structure changes:

// app/api/users/[id]/route.ts export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { const user = await fetchUser(params.id) return NextResponse.json(user) }

Handling Data Fetching Migration

Server-Side Rendering Migration

The most complex part of migration involves converting data fetching patterns. Here's how to handle different scenarios:

Simple getServerSideProps conversion:

// Before (Pages Router) export async function getServerSideProps(context) { const { id } = context.params const user = await fetchUser(id) return { props: { user } } } // After (App Router) async function UserPage({ params }: { params: { id: string } }) { const user = await fetchUser(params.id) return <UserProfile user={user} /> }

Handling redirects and not found:

// App Router with error handling async function UserPage({ params }: { params: { id: string } }) { const user = await fetchUser(params.id) if (!user) { notFound() // Triggers 404 page } if (user.isPrivate && !user.canAccess) { redirect('/login') // Server-side redirect } return <UserProfile user={user} /> }

Static Generation Migration

Converting getStaticProps and getStaticPaths:

// Before (Pages Router) export async function getStaticPaths() { const posts = await fetchAllPosts() const paths = posts.map(post => ({ params: { slug: post.slug } })) return { paths, fallback: 'blocking' } } export async function getStaticProps({ params }) { const post = await fetchPost(params.slug) return { props: { post }, revalidate: 60 } } // After (App Router) export async function generateStaticParams() { const posts = await fetchAllPosts() return posts.map(post => ({ slug: post.slug })) } async function BlogPost({ params }: { params: { slug: string } }) { const post = await fetchPost(params.slug) return <PostContent post={post} /> } // Configure revalidation export const revalidate = 60

Client-Side Data Fetching

For client-side data fetching, you'll need to mark components with 'use client':

'use client' import { useState, useEffect } from 'react' export default function ClientDataComponent() { const [data, setData] = useState(null) useEffect(() => { fetch('/api/data') .then(res => res.json()) .then(setData) }, []) if (!data) return <div>Loading...</div> return <div>{/* Render data */}</div> }

Common Migration Pitfalls and Solutions

Pitfall 1: Mixing Server and Client Components

One of the most common issues is attempting to use client-side features in Server Components:

// ❌ This will cause an error export default async function ServerComponent() { const [state, setState] = useState(null) // Error: useState in Server Component const data = await fetchData() return <div onClick={() => setState(data)}>Click me</div> // Error: onClick in Server Component }

Solution: Separate server and client concerns:

// ✅ Server Component async function ServerComponent() { const data = await fetchData() return <ClientComponent initialData={data} /> } // ✅ Client Component 'use client' function ClientComponent({ initialData }) { const [state, setState] = useState(initialData) return <div onClick={() => setState(newData)}>Click me</div> }

Pitfall 2: Incorrect Error Boundary Placement

App Router requires explicit error boundaries for each route segment:

// app/dashboard/error.tsx 'use client' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div> <h2>Something went wrong!</h2> <button onClick={reset}>Try again</button> </div> ) }

Pitfall 3: Metadata Configuration Issues

Forgetting to properly configure metadata can hurt SEO:

// ✅ Static metadata export const metadata = { title: 'My Page', description: 'Page description', } // ✅ Dynamic metadata export async function generateMetadata({ params }) { const post = await fetchPost(params.id) return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.image], }, } }

Pitfall 4: Route Handler Parameter Access

API route parameters are accessed differently:

// ❌ Wrong way export async function GET(req) { const { id } = req.query // This doesn't exist } // ✅ Correct way export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { const id = params.id const searchParams = request.nextUrl.searchParams const query = searchParams.get('query') }

Testing Your Migration

Automated Testing Strategy

Create a comprehensive testing plan to ensure your migration doesn't break existing functionality:

// __tests__/migration.test.js import { render, screen } from '@testing-library/react' import HomePage from '../app/page' // Mock fetch for server components global.fetch = jest.fn() describe('Migration Tests', () => { it('renders homepage correctly', async () => { fetch.mockResolvedValue({ ok: true, json: async () => ({ data: 'test' }) }) render(await HomePage()) expect(screen.getByText('Welcome')).toBeInTheDocument() }) })

Performance Validation

Compare performance metrics before and after migration:

# Run Lighthouse audits npx lighthouse http://localhost:3000 --output=json --output-path=./before-migration.json # After migration npx lighthouse http://localhost:3000 --output=json --output-path=./after-migration.json

Monitor key metrics:

  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Time to Interactive (TTI)
  • Bundle size changes

Best Practices for App Router Migration

1. Gradual Migration Strategy

Don't attempt to migrate everything at once. Start with:

  1. Simple static pages
  2. Pages with basic data fetching
  3. Complex dynamic routes
  4. API routes with dependencies
  5. Pages with complex client-side interactions

2. Leverage Colocated Files

Take advantage of App Router's file colocation:

app/
  dashboard/
    page.tsx          // Route component
    loading.tsx       // Loading UI
    error.tsx         // Error boundary
    not-found.tsx     // 404 page
    components/       // Page-specific components
      chart.tsx
      table.tsx
    styles/           // Page-specific styles
      dashboard.css

3. Optimize Server Component Usage

Maximize the benefits of Server Components:

// ✅ Keep data fetching in Server Components async function ProductList() { const products = await fetchProducts() return ( <div> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> ) } // ✅ Use Client Components only when necessary 'use client' function ProductCard({ product }) { const [liked, setLiked] = useState(false) return ( <div> <h3>{product.name}</h3> <button onClick={() => setLiked(!liked)}> {liked ? '❤️' : '🤍'} </button> </div> ) }

4. Implement Proper Error Handling

Create a robust error handling strategy:

// app/error.tsx - Global error boundary 'use client' export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { // Log error to monitoring service console.error('Global error:', error) }, [error]) return ( <html> <body> <div className="error-container"> <h2>Something went wrong!</h2> <button onClick={reset}>Try again</button> </div> </body> </html> ) }

5. Configure Proper Caching

Understand and configure caching behavior:

// Static rendering (default) export default async function StaticPage() { const data = await fetch('https://api.example.com/static-data') return <div>{data}</div> } // Dynamic rendering export const dynamic = 'force-dynamic' export default async function DynamicPage() { const data = await fetch('https://api.example.com/dynamic-data') return <div>{data}</div> } // Revalidated static rendering export const revalidate = 3600 // Revalidate every hour export default async function RevalidatedPage() { const data = await fetch('https://api.example.com/data') return <div>{data}</div> }

Post-Migration Optimization

Once your migration is complete, focus on optimization opportunities unique to App Router:

Bundle Size Optimization

App Router enables better code splitting through Server Components:

// Heavy client-side library only loads when needed 'use client' import { Chart } from 'heavy-chart-library' export default function ChartComponent({ data }) { return <Chart data={data} /> }

SEO Enhancements

Leverage the improved metadata API:

export async function generateMetadata({ params, searchParams }) { const product = await fetchProduct(params.id) return { title: `${product.name} | Your Store`, description: product.description, keywords: product.tags.join(', '), openGraph: { title: product.name, description: product.description, images: product.images.map(img => ({ url: img.url, width: img.width, height: img.height, alt: img.alt, })), }, twitter: { card: 'summary_large_image', title: product.name, description: product.description, images: [product.featuredImage], }, } }

Performance Monitoring

Set up monitoring to track the impact of your migration:

// app/layout.tsx import { Analytics } from '@vercel/analytics/react' export default function RootLayout({ children }) { return ( <html> <body> {children} <Analytics /> </body> </html> ) }

The migration from Pages Router to App Router represents a significant architectural shift that, while complex, provides substantial long-term benefits. The key to success lies in methodical planning, gradual implementation, and thorough testing at each stage. Start with simple pages, establish patterns for your team, and progressively tackle more complex scenarios.

Remember that both routers can coexist during migration, allowing you to move at a comfortable pace without disrupting production functionality. Focus on leveraging Server Components for improved performance, implement proper error boundaries for better user experience, and take advantage of the enhanced metadata API for SEO benefits.

If you're dealing with a complex Next.js application that includes payment integration, subscription management, or other critical business logic, consider getting expert assistance to ensure a smooth migration without disrupting your revenue streams. Our Next.js optimization service specializes in App Router migrations for production applications, helping teams navigate the complexities while maintaining system reliability and performance.

Related Articles

Migrating Stripe Integration from Next.js Pages Router to App Router
Next.js
Migrating Stripe Integration from Next.js Pages Router to App Router
When Next.js 13 introduced the App Router with React Server Components, many developers found themselves at a crossroads: continue with the familiar Pages Route...
Optimizing Next.js Server Components for Better Performance
Next.js
Optimizing Next.js Server Components for Better Performance
Server Components in Next.js App Router promise faster loading times and better SEO, but many developers struggle to realize these benefits in production. A rec...
Next.js Image Optimization: Beyond next/image Defaults
Next.js
Next.js Image Optimization: Beyond next/image Defaults
Most developers discover Next.js image optimization when their Lighthouse scores plummet or users complain about slow loading times. You implement next/image,...

Need Expert Implementation?

I provide professional Stripe integration and Next.js optimization services with fixed pricing and fast delivery.