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.
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 (
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
- Create
app/folder side-by-side withpages/. - Migrate one top-level route group at a time (e.g.,
/marketing). - Use feature flags for UI changes.
- 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 telemetryand 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.
Scaling Next.js Applications on the Edge
A deep dive into caching strategies...
Read ArticleScaling Next.js Applications on the Edge
A deep dive into caching strategies...
Read Article