nextjsmigrationapp-router

Migrating to Next.js 15 App Router: A Practical Guide

A practical migration playbook from Pages Router (or other frameworks) to Next.js 15 App Router — covering RFC, server components, data fetching, layouts, and fallback strategies.

Johan S.
Johan S.
Backend Lead
Migrating to Next.js 15 App Router: A Practical Guide
March 14, 2025
9 min read

Migrating to Next.js 15 App Router: A Practical Guide

Next.js 15's App Router brings Server Components, nested layouts, and stronger edge/runtime choices. This guide is a pragmatic migration playbook for medium-to-large apps.


Why migrate?

  • Server Components reduce client bundle sizes.
  • Nested layouts simplify complex shell and UI composition.
  • Fine-grained rendering control (per-route caching & runtime).
  • Edge runtime support for low-latency global responses.

Pre-migration RFC (must-have)

Create an RFC that covers:

  • Goals (performance, DX, infra)
  • Supported runtimes (edge vs node)
  • Caching strategy per-route
  • Authentication flow (middleware vs server actions)
  • SEO and redirect strategy
  • Rollback plan & feature flags

Step 1 — Audit & categorization

Categorize pages:

  • Marketing pages → SSG / ISR
  • User dashboard → SSR/Server Components + streaming
  • Forms / complex client interactivity → Client Components where needed
  • Legacy third-party widgets → Evaluate isolation or iframe

Audit list:

  • Third-party libs with DOM-only assumptions
  • Custom Webpack plugins / loaders
  • Runtime-specific native modules

Step 2 — Create app/ layout shell

Start by creating a top-level app/layout.tsx that mirrors your global HTML structure.

`` sx // app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) }


Add streaming-friendly placeholders (skeletons) in nested layouts for faster time-to-first-byte perception.

---

## Step 3 — Data fetching & Server Components

Convert heavy-data pages to server components:

``	sx
// app/dashboard/page.tsx (server component)
import { getUserDashboard } from '@/lib/api'

export default async function Dashboard() {
  const data = await getUserDashboard()
  return <DashboardShell data={data} />
}
``
**Key:** Server components can call server-side libraries (DB clients) directly — keep secrets on server only.

---

## Step 4 — Client Components & interactivity

When client-side hooks or effects are required, use explicit client components:

``	sx
'use client'
import { useState, useEffect } from 'react'

export default function Notifications() {
  const [list, setList] = useState([])
  useEffect(() => {
    fetch('/api/notifications').then(r => r.json()).then(setList)
  }, [])
  return <NotificationList items={list} />
}

Minimize surface area by isolating client components and passing data from the server where possible.


Step 5 — Routing differences & redirects

App Router uses filesystem-based routing with optional segments and parallel routes. Migrate redirects from next.config.js and keep canonical mapping.

Example redirect config (next.config.js):

``js module.exports = { async redirects() { return [ { source: '/old-pricing', destination: '/pricing', permanent: true }, ] }, }


---

## Step 6 — Authentication patterns

Options:
- **Middleware-based** for route gating (fast at Edge).
- **Server component check** for server-rendered pages (redirect from server).
- **Opaque token cookies** for SSR flows — avoid localStorage for auth.

Example server redirect:

``	sx
// app/profile/page.tsx
import { redirect } from 'next/navigation'
export default async function Profile() {
  const user = await getUserFromCookie()
  if (!user) redirect('/signin')
  return <ProfileShell user={user} />
}

Step 7 — Incremental migration approach

  1. Create app/ folder side-by-side with pages/.
  2. Migrate one top-level route group at a time (e.g., /marketing).
  3. Use feature flags for UI changes.
  4. Keep tests green and run synthetic Lighthouse checks on each migrated route.

Step 8 — Common pitfalls & fixes

  • Third-party libs expecting window — lazy-load inside client components.
  • Form handling — consider Server Actions (if available) or API routes for progressive migration.
  • Bundle size regressions — analyze bundle with next build && next telemetry and keep client component count low.

Migration checklist

  • RFC completed and approved
  • App shell & top-level layout implemented
  • Critical pages ported (marketing, core auth flows)
  • Tests updated (E2E + integration)
  • Lighthouse CI configured for migrated routes
  • Rollback plan validated

Closing notes

Migrating to App Router is a strategic investment — you gain server components, better routing primitives, and performance benefits. Start with low-risk pages, keep the migration iterative, and instrument heavily.

If you want, we can:

  • Create an automated migration plan for your repo
  • Provide a 2-week migration sprint to port the first route group
  • Run a Lighthouse CI baseline and post-migration comparison

Enjoyed this post?

Join 10,000+ developers receiving our latest engineering deep dives and tutorials directly in their inbox.

Read Next

More articles from our team.

View all posts
Engineering

Scaling Next.js Applications on the Edge

A deep dive into caching strategies...

Read Article
Engineering

Scaling Next.js Applications on the Edge

A deep dive into caching strategies...

Read Article