Next.js 16 App Router: Migration Guide from Pages Router
By Eoin Villarin, 2026-04-05, updated 2026-10-28
Still on the Pages Router? Migrate to the Next.js 16 App Router route by route, with clear steps for layouts, data fetching, caching, routing hooks and proxy.ts.
How do I migrate from the Pages Router to the Next.js 16 App Router?
Pages Router apps can migrate one route at a time, because the app and pages directories run side by side. A Next.js App Router migration starts by upgrading to Next.js 16 and running the codemods, then adding a root layout, replacing getServerSideProps and getStaticProps with async Server Components and fetch caching, and adopting Cache Components later.
Key takeaways
- In Next.js 16, params, searchParams, cookies() and headers() must be awaited, as synchronous compatibility mode is gone.
- Next.js 16 renames middleware.ts to proxy.ts, which runs only on the Node.js runtime.
- getStaticPaths becomes generateStaticParams, and getStaticProps with revalidate becomes fetch with next.revalidate.
- Keep _app and _document until the last page moves, because root layout styles do not apply to the pages directory.
- Next.js 16 requires Node.js 20.9 or newer and TypeScript 5.1 or newer, and Turbopack is the default bundler.
A Next.js App Router migration is no longer a question of if, but when. Next.js 16 still runs the Pages Router, yet every major feature shipped in the last three years, from Server Actions to Cache Components and the new prefetching model, lives in the app/ directory. If your project still depends on getServerSideProps and _app.tsx, you are maintaining code that is slowly becoming legacy.
The good news is that you do not need a big bang rewrite. Both routers run side by side, so you can move one route at a time, ship it and measure it while the site stays live.
In this guide you will learn what changed in Next.js 16 that affects a migration, how to set up the root layout, how to replace every Pages Router data fetching function, how caching works now, and the pitfalls that break most first attempts.
Why is a Next.js App Router migration worth it in 2026?
The App Router is not just a new folder structure. It is built on React Server Components, streaming and nested layouts, which means less JavaScript in the browser and data fetching that happens next to your data. If the server and client split is new to you, my guide to React 19 Server Components covers the mental model first.
The bigger reason is direction. Next.js 16 made Turbopack the default bundler, stabilized React Compiler support and introduced Cache Components. With the Next.js 16.4 release in October 2026, the team said Cache Components will become the default in Next.js 17, and new create-next-app projects already enable it. None of that reaches the pages/ directory.
- Smaller client bundles: Server Components send no component JavaScript to the browser.
- Nested layouts: shared UI stays mounted between navigations instead of re-rendering.
- Streaming: slow data no longer blocks the whole page, thanks to Suspense boundaries.
- Built-in metadata API: titles, Open Graph tags and canonicals without
next/head.
What changed in Next.js 16 before you migrate?
If you last looked at the App Router in Next.js 13 or 14, several defaults have moved. Check them before you touch a single route, because they change what your migrated code should look like.
| Pages Router | App Router in Next.js 16 |
|---|---|
pages/_app.tsx and pages/_document.tsx | app/layout.tsx root layout |
next/head | metadata export or generateMetadata |
getServerSideProps | Async Server Component, uncached by default |
getStaticProps with revalidate | fetch with next.revalidate, or 'use cache' with cacheLife |
getStaticPaths | generateStaticParams |
useRouter from next/router | useRouter, usePathname, useSearchParams from next/navigation |
pages/api/* | route.ts Route Handlers |
middleware.ts | proxy.ts on the Node.js runtime |
pages/404.tsx and _error.tsx | not-found.tsx and error.tsx |
Three Next.js 16 changes catch people most often:
- Async request APIs only.
params,searchParams,cookies(),headers()anddraftMode()must be awaited. The synchronous compatibility mode from Next.js 15 is gone. - Middleware is now Proxy. Rename
middleware.tstoproxy.tsand the exported function toproxy. Proxy runs on the Node.js runtime only. - Turbopack by default. If a plugin injects a custom webpack config,
next buildfails until you migrate it or opt out with--webpack.
Next.js 16 also requires Node.js 20.9 or newer and TypeScript 5.1 or newer. Node.js 20 is now end of life, so move to Node.js 22 or 24 at the same time.
How to run the Pages Router and App Router side by side
The app/ and pages/ directories are designed to coexist. Routes you create in app/ take over their URLs, and everything else keeps working from pages/.
Step 1: Upgrade and run the codemods
Start on a clean branch and upgrade Next.js first, without moving any routes. The upgrade codemod handles the Turbopack config, the next lint removal and the middleware rename. A second codemod converts synchronous request API access.
npx @next/codemod@canary upgrade latest
npx @next/codemod@canary next-async-request-api .
npx next typegen
The last command generates global PageProps, LayoutProps and RouteContext type helpers, so new pages get typed params. If you use a coding agent, npx next@canary upgrade --agent=latest prepares the same steps for it, but review every diff yourself.
Step 2: Create the root layout
Create app/layout.tsx. It replaces both _app and _document for routes inside app/, and it must render the html and body tags itself.
import type { Metadata } from 'next'
import './globals.css'
import { Providers } from './providers'
export const metadata: Metadata = {
title: { default: 'Acme Store', template: '%s | Acme Store' },
description: 'Handmade goods shipped worldwide.',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
Keep _app and _document until the last page has moved. Styles imported in the root layout do not apply to pages/, so deleting the old files early breaks every route you have not migrated yet.
How to migrate _app providers and next/head
Context providers for themes, auth, cart state or React Query need state and effects, so they must live in a Client Component. Put them in one file and import it from the root layout.
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const [client] = useState(() => new QueryClient())
return <QueryClientProvider client={client}>{children}</QueryClientProvider>
}
Keep the 'use client' boundary as low in the tree as you can. Everything a client file imports joins the client bundle.
For the head, replace next/head with the metadata API. Static pages export a metadata object, and dynamic pages use generateMetadata, which receives the same async params as the page. My Next.js SEO guide for the App Router covers canonicals, sitemaps and Open Graph images in detail.
How to replace getServerSideProps, getStaticProps and getStaticPaths
This is where most of the work happens. The App Router has no special data functions. A page is an async Server Component that fetches what it needs, and caching is chosen per request or per component.
getServerSideProps becomes an async component
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
export default async function Dashboard() {
const session = (await cookies()).get('session')?.value
const res = await fetch('https://api.example.com/projects', {
headers: { Authorization: `Bearer ${session}` },
})
const projects: { id: string; name: string }[] = await res.json()
return (
<ul>
{projects.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
Since Next.js 15, fetch is not cached by default, so this behaves like getServerSideProps without any extra flag. Request data that used to come from req now comes from await cookies() and await headers().
getStaticProps with revalidate becomes a cached fetch
To keep incremental static regeneration, opt the request into the cache with a revalidation window and a tag you can purge later:
const res = await fetch('https://cms.example.com/posts', {
next: { revalidate: 3600, tags: ['posts'] },
})
getStaticPaths becomes generateStaticParams
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function Post(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
const post = await getPost(slug)
if (!post) notFound()
return <article>{post.title}</article>
}
Slugs you do not return are rendered on demand, which replaces fallback: true and 'blocking'. Calling notFound() from next/navigation when the data does not exist gives you the same result as fallback: false.
How does caching work after the migration?
Caching is the part of the App Router that changed most between versions, so be deliberate. You have two models in Next.js 16, and you should pick one per project rather than mixing them by accident.
- The classic model:
fetchoptions (cache: 'force-cache',next.revalidate,next.tags) plus route segment config such asexport const revalidate. This maps closely to the Pages Router and is the easier first step. - Cache Components: enable
cacheComponents: trueand mark cached functions or components with'use cache'andcacheLife. Everything else runs at request time, and Next.js prerenders a static shell around it.
import { cacheLife, cacheTag } from 'next/cache'
async function getPosts() {
'use cache'
cacheLife('hours')
cacheTag('posts')
const res = await fetch('https://cms.example.com/posts')
return res.json()
}
Migrate routes on the classic model first, then adopt Cache Components as a separate project. Changing routers and caching models in the same pull request makes every bug twice as hard to trace.
When you do switch, note that Cache Components rejects route segment exports like dynamic, revalidate, fetchCache and dynamicParams. Invalidation also changed: revalidateTag now takes a cache profile as its second argument, such as revalidateTag('posts', 'max'), and updateTag gives read-your-writes behavior inside Server Actions.
How to migrate routing hooks, API routes and middleware
Routing hooks
The useRouter from next/navigation is a different API. It has no pathname, query, asPath or isReady. Use usePathname() for the path, useSearchParams() for the query string and useParams() for dynamic segments. For components shared by both routers during the transition, import useRouter from next/compat/router.
API routes become Route Handlers
Route Handlers use the Web Request and Response APIs instead of Node's req and res:
// app/api/newsletter/route.ts
export async function POST(request: Request) {
const { email } = await request.json()
if (!email) return Response.json({ error: 'Email required' }, { status: 400 })
await subscribe(email)
return Response.json({ ok: true })
}
Your existing pages/api routes keep working, so move them last. Many disappear entirely once Server Components fetch the data directly.
Middleware becomes proxy.ts
Rename the file and the function. The logic stays the same, but proxy.ts always runs on Node.js. If you rely on the Edge runtime, keep middleware.ts for now, since it is deprecated rather than removed.
What are the most common App Router migration pitfalls?
- Importing from
next/routerinapp/. It throws at runtime. Search the codebase for it before each route moves. - Forgetting to await
params. In Next.js 16 it is a Promise. Runnpx next typegenand let TypeScript catch it. - Passing non-serializable props. Functions and class instances cannot cross from a Server Component to a Client Component.
- Losing SEO tags. A page without a
metadataexport falls back to the root layout's title, which can create duplicate titles across hundreds of URLs. - Hard navigations between routers. Links between an
app/route and apages/route trigger a full page load, so migrate tightly linked sections together.
Before each release, compare the migrated route against the old one: rendered HTML, status codes, redirects, canonical tags and Core Web Vitals. A route that looks identical can still regress on Interaction to Next Paint if a provider moved too high in the tree, so read my INP guide if your field data dips.
Frequently asked questions
Is the Pages Router deprecated in Next.js 16?
No. The Pages Router is still supported in Next.js 16 and existing apps keep working. However, new features such as Cache Components, Partial Prerendering and the new prefetching model only work in the App Router. Treat the Pages Router as stable but finished, and plan a gradual move rather than an urgent one.
How long does a Next.js App Router migration take?
It depends on page count and how much logic sits in _app, API routes and getServerSideProps. A marketing site with a handful of templates can move in days, while a large app with auth, i18n and custom middleware often takes several sprints. Migrating route by route keeps each step shippable.
Do I need to adopt Cache Components during the migration?
No, and I would not. Move routes using the classic fetch caching options first, because they map directly to getStaticProps and getServerSideProps. Once every route lives in app/, enable Cache Components as a separate project before Next.js 17 makes it the default.
Will migrating to the App Router hurt my SEO?
Not if you keep URLs, status codes, titles, descriptions and canonicals identical. The App Router renders HTML on the server just like the Pages Router. The risks are missing metadata exports and changed redirects, so crawl the site before and after each release and compare the results.
Next steps for your migration
A successful Next.js App Router migration is boring on purpose: upgrade first, add a root layout, move one route at a time, keep caching simple, then adopt Cache Components once everything lives in app/. Follow the official Pages to App Router migration guide alongside this checklist and you avoid the surprises that turn a two-week job into a quarter.
If you would rather hand the migration to someone who has done it, get in touch through my contact form with your repository size and deadline. You can also browse my web development case studies to see how I approach rebuilds for clients in Australia, the US and beyond.