# Next.js 16 App Router: Migration Guide from Pages Router

By [Eoin Villarin](https://amvprosox.com/about). Published 2026-04-05, updated 2026-10-28. Canonical: https://amvprosox.com/blog/nextjs-15-app-router-migration-guide

> 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](https://amvprosox.com/blog/react-19-server-components-complete-guide) covers the mental model first.

![Developer seen from behind drawing a flowchart on a whiteboard to map routes](https://amvprosox.com/api/img/1793146639919-7j8q5s)
*Map every Pages Router concept to its App Router equivalent first. Photo: rawpixel, CC0*

The bigger reason is direction. [Next.js 16](https://nextjs.org/blog/next-16) made Turbopack the default bundler, stabilized React Compiler support and introduced Cache Components. With the [Next.js 16.4 release](https://nextjs.org/blog/next-16-4) 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()` and `draftMode()` must be awaited. The synchronous compatibility mode from Next.js 15 is gone.

- **Middleware is now Proxy.** Rename `middleware.ts` to `proxy.ts` and the exported function to `proxy`. Proxy runs on the Node.js runtime only.

- **Turbopack by default.** If a plugin injects a custom webpack config, `next build` fails 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 (

        {children}

  )
}
```

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.

![Modular glass panels on a modern building facade, like a component tree split into parts](https://amvprosox.com/api/img/1793146641163-25jztq)
*Keep client boundaries small so most of the tree stays on the server. Photo: Ake, CC0*

```
'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 {children}
}
```

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](https://amvprosox.com/blog/nextjs-seo-complete-guide-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 (

      {projects.map((p) =>
- {p.name})}

  )
}
```

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) {
  const { slug } = await props.params
  const post = await getPost(slug)
  if (!post) notFound()
  return {post.title}
}
```

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.

![Smartphone analytics dashboard on a desk, used to measure each migrated route](https://amvprosox.com/api/img/1793146642360-7la0o4)
*Measure each migrated route before moving to the next one. Photo: rawpixel, CC0*

- **The classic model:** `fetch` options (`cache: 'force-cache'`, `next.revalidate`, `next.tags`) plus route segment config such as `export const revalidate`. This maps closely to the Pages Router and is the easier first step.

- **Cache Components:** enable `cacheComponents: true` and mark cached functions or components with `'use cache'` and `cacheLife`. 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/router` in `app/`.** 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. Run `npx next typegen` and 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 `metadata` export 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 a `pages/` 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](https://amvprosox.com/blog/interaction-to-next-paint-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](https://nextjs.org/docs/app/guides/migrating/app-router-migration) 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](https://amvprosox.com/#contact-form) with your repository size and deadline. You can also browse my [web development case studies](https://amvprosox.com/case-studies) to see how I approach rebuilds for clients in Australia, the US and beyond.
