Technology

Next.js 15 App Router: Migration Guide

Step-by-step guide to migrating from Pages Router to App Router, handling layouts, streaming, and server actions.

All articles
TechnologyNexaEx TeamJanuary 1, 2026 9 min read
Next.js 15 App Router: Migration Guide

Migrating to Next.js 15 App Router

Next.js 13+ introduced the App Router as the future of Next.js. If you're on Pages Router, migration is inevitable. Here's how to do it smoothly.

Why Migrate?

Server components: Write components that run only on server. No client-side JavaScript overhead.

Streaming: Send HTML to browser while backend fetches data. Faster perceived performance.

Simpler routing: File-based routes, no config. /app/blog/page.tsx becomes /blog.

Layouts: Persistent layouts without state resets.

High-Level Differences

FeaturePages RouterApp Router
Route definition/pages/blog/post.tsx/app/blog/[slug]/page.tsx
LayoutsPer-page or hoc/app/layout.tsx (hierarchical)
Data fetchinggetServerSideProps, getStaticPropsfetch() in components
Client-side stateLocal hooksuse() hook (with Suspense)
Middleware/pages/_middleware.ts/middleware.ts (root)

Step 1: Set Up App Router

Create /app directory alongside /pages. Both coexist during migration:

src/
  app/           # New app router
  pages/         # Old pages router (temporary)

Next.js 13+ prioritizes app-router. Gradually move routes from pages to app.

Step 2: Create Root Layout

Replace /pages/_app.tsx and /pages/_document.tsx with /app/layout.tsx:

// app/layout.tsx
export const metadata = {
  title: 'My App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Step 3: Move Pages

For each page in /pages, create a corresponding file in /app:

pages/blog/index.tsx         →  app/blog/page.tsx
pages/blog/[slug].tsx        →  app/blog/[slug]/page.tsx
pages/admin/[...slug].tsx    →  app/admin/[[...slug]]/page.tsx

Step 4: Server Components by Default

App Router pages are server components by default:

// app/blog/page.tsx - Server component
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json())

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  )
}

No getStaticProps or getServerSideProps needed. Simpler, clearer.

Step 5: Client Interactions

For interactivity (state, effects, event handlers), mark components with 'use client':

// app/components/counter.tsx
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

Keep client components small. Parent component can remain server component, embedding client components where needed.

Step 6: Handle Data Fetching

No more getStaticProps. Fetch in server components:

// app/blog/page.tsx
export const revalidate = 60 // ISR: revalidate every 60 seconds

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // or use revalidate export
  }).then(res => res.json())

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Step 7: Implement Streaming

Wrap slow-loading sections in <Suspense>:

import { Suspense } from 'react'

async function Posts() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json())
  return posts.map(post => <article key={post.id}>{post.title}</article>)
}

export default function BlogPage() {
  return (
    <>
      <h1>Blog</h1>
      <Suspense fallback={<p>Loading posts...</p>}>
        <Posts />
      </Suspense>
    </>
  )
}

Browser gets HTML immediately with loading UI. When data fetches, it streams HTML without re-rendering the page.

Step 8: Server Actions (Optional)

Server actions let you call server functions from client components without API routes:

// app/actions.ts
'use server'

export async function submitForm(formData: FormData) {
  const name = formData.get('name')
  // Process on server
  return { success: true, name }
}

// app/form.tsx
'use client'

import { submitForm } from '@/app/actions'

export default function Form() {
  return (
    <form action={submitForm}>
      <input name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}

Common Pitfalls

  1. Forgetting 'use client': Client interactions fail silently if you forget the directive.
  2. Props from server to client: Server component data must be JSON-serializable; functions can't pass to clients.
  3. Middleware changes: Old-style middleware won't work. Rewrite in /middleware.ts.
  4. Dynamic imports: Use dynamic() from next/dynamic for large client components.

Migration Order

  1. Migrate static pages first (no data fetching).
  2. Then ISR/SSG pages.
  3. Finally SSR pages (most complex).
  4. Keep Pages Router for complex API routes until you're comfortable with App Router.

Performance Gains

Typical results after migration:

  • Time to first byte: 20-30% faster (server rendering closer to data).
  • JavaScript sent to browser: 30-50% less (server components don't ship code).
  • Perceived load time: 15-25% faster (streaming).

These gains compound. A successful migration is worth the effort.

Frequently asked questions

Can I use Pages Router and App Router together?

Yes. Both coexist during migration. App Router takes priority. Gradually move routes from pages/ to app/.

Do I still need getServerSideProps in App Router?

No. Fetch data directly in server components. App Router simplifies data fetching significantly.

What happens if I forget 'use client' on a component with state?

The component will error at runtime. useState, useEffect, and other hooks require the 'use client' directive.

Let's build your next idea

One conversation to scope the work, meet the team, and get a proposal — usually within two business days.