Next.js Pages Router to App Router: Complete Migration Guide (2026)
Author
Muhammad Awais
Published
May 22, 2026
Reading Time
9 min read
Views
18k

Why the App Router Migration Can No Longer Wait
If you have been running a Next.js project on the Pages Router and telling yourself you will migrate "when things settle down," Next.js 16 just ended that window. Released on March 18, 2026, Next.js 16.2 made four architectural changes at once: caching switched from opt-out to opt-in, Turbopack became the default bundler replacing Webpack, middleware.ts began its deprecation path toward proxy.ts, and React 19.2 shipped as the bundled runtime. Any one of those changes would justify a migration guide. All four together mean you need a clear picture of what changed and exactly what to do before you touch a line of code.
The good news is that the migration path is well-defined. Next.js supports both routers simultaneously, which means you can move incrementally one page at a time rather than rewriting everything in one risky push. This guide walks through the entire process in the order that causes the fewest surprises. If you are starting a new project in 2026, skip directly to Step 2. If you are migrating an existing Pages Router codebase, read from Step 1.
What Actually Changed in Next.js 16: The Four Things That Matter
Before touching your codebase, understand what changed at the framework level. These are not minor API renames they are default behavior changes that affect every existing Next.js application.
Caching is now opt-in, not opt-out: In Next.js 13 through 15,
fetch()calls were cached by default. You had to explicitly write{ cache: 'no-store' }to get dynamic behavior. Next.js 16 reverses this. Everything is dynamic by default. If you want caching, you opt in using the newuse cachedirective or thecacheLifeandcacheTagAPIs. This is architecturally the right call, but it means every existing cached fetch in your codebase needs an audit.Turbopack is the default bundler: The Rust-based Turbopack is now stable for both development and production builds in Next.js 16. Dev server startup times improved by approximately 400% compared to the previous Webpack-based setup. If you have custom Webpack configuration in your
next.config.ts, that configuration will not apply under Turbopack and you will need to migrate or use an explicit fallback flag.Async APIs are now strictly required:
cookies(),headers(),draftMode(),params, andsearchParamsare all async in Next.js 16. Next.js 15 introduced these as async but kept a synchronous compatibility shim. Next.js 16 removes the shim entirely synchronous access now throws a runtime error. This is the most common source of immediate breakage when upgrading.React 19.2 is the bundled runtime: React 19.2 ships with
useEffectEvent, the<Activity>component, and View Transitions support integrated at the framework level. If your codebase depends on React 18-specific behavior for concurrent features, audit those components before upgrading. The details of what React 19 changed at the API level are covered in our complete React 19 features guide.
Step 1: Preparation Before You Write a Single Line
Do not start migrating until these prerequisites are in place. Skipping this step is what causes migrations to turn into multi-week incidents.
Pin your Node.js version to 20.9.0 or higher. Next.js 16 dropped Node.js 18 support entirely. Check your current version with
node -v. If you are on 18, update first. Your CI pipeline, Dockerfile, and any serverless deployment environments all need to match.Set up a preview environment if you do not have one. Running this migration without a staging environment where you can test each page before it hits production is a recoverable mistake once. Do not let it become twice.
Run the official codemod first. Before manually touching anything, run
npx @next/codemod@latest upgradein your project root. This handles the mechanical changes async API wrappers, deprecated import renames, and config key updates. Review the diff carefully before committing. The codemod handles roughly 80% of the breaking changes. The remaining 20% requires human judgment.Create the
app/directory alongside your existingpages/directory. Do not delete or movepages/yet. Next.js runs both routers simultaneously. Your old pages continue working while you migrate one route at a time.
Step 2: Migrating Your Layout and Global Files
The first real migration work is converting your pages/_app.tsx and pages/_document.tsx into an App Router root layout. This is where the biggest mental model shift lives.
In the Pages Router, _app.tsx wrapped every page with shared providers, global styles, and persistent UI like navigation. In the App Router, that role belongs to app/layout.tsx. Create it at the root of your app/ directory:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{/* Move your providers and navigation here */}
{children}
</body>
</html>
);
}Everything in pages/_document.tsx — custom <html> attributes, fonts, meta tags moves directly into this layout file. The <Head> component from next/head is replaced by the metadata export, which Next.js handles automatically at the framework level.
Step 3: Converting Pages One at a Time
This is the core of the migration. For each page in pages/, you create a corresponding file in app/ and convert it to use the App Router conventions. The safest order is: start with your simplest static pages, then move to data-fetching pages, and leave authenticated or complex interactive pages for last.
The key conversion rules are straightforward once you internalize them:
getStaticProps→ async Server Component with direct fetch: Remove the function entirely. Fetch your data directly inside the component body usingasync/await. Add{ next: { revalidate: 3600 } }to the fetch call if you need ISR behavior.getServerSideProps→ async Server Component with{ cache: 'no-store' }: Same pattern fetch directly in the component. Pass{ cache: 'no-store' }to the fetch call to ensure fresh data on every request.getStaticPaths→generateStaticParams(): Export this function from yourpage.tsxfile and return an array of param objects. The structure is the same as before but the function name changes.Client-side interactivity → add
'use client'at the top: Any component that usesuseState,useEffect, event handlers, or browser APIs needs the'use client'directive as the first line of the file. Server Components cannot use these features.
Step 4: Fixing the Async API Breaking Changes
This is the step where most migrations break if the codemod did not catch every instance. In Next.js 16, cookies(), headers(), params, and searchParams are all Promises. Every access point needs an await:
// BEFORE — throws in Next.js 16
import { cookies } from 'next/headers';
const cookieStore = cookies();
const token = cookieStore.get('token');
// AFTER — correct in Next.js 16
import { cookies } from 'next/headers';
const cookieStore = await cookies();
const token = cookieStore.get('token');The same applies to dynamic route params. If your page receives params as a prop, you now need to await it before accessing any property. This pattern also shows up in API routes, Server Actions, and middleware. Search your codebase for every cookies(), headers(), and params. access point — not just the ones the codemod flagged. If your API routes return data with timestamps that users need to inspect or convert, our Unix Timestamp Converter makes it easy to decode any epoch value from a response without leaving your workflow.
Step 5: Migrating Data Fetching and the New Caching Model
The caching inversion in Next.js 16 requires an explicit audit of every fetch call in your application. The old mental model was "cached unless I say otherwise." The new mental model is "dynamic unless I say otherwise." Here is the complete mapping:
Old:
fetch(url)(implicitly cached) → New:fetch(url, { next: { revalidate: false } })or use theuse cachedirective on the componentOld:
fetch(url, { cache: 'no-store' })→ New:fetch(url)— this is now the defaultOld:
unstable_cache()→ New:use cachedirective — apply at the file, function, or component level
For API routes that return JSON responses you need to type, our JSON to TypeScript Interface Converter converts any API response payload into clean TypeScript interfaces in one paste — useful when you are rebuilding Route Handlers and want to lock in types without writing them by hand.
Step 6: Handling Scheduled Jobs and Background Tasks
If your Next.js application uses API routes to run scheduled background tasks like sending emails, syncing data, or cleaning up database records on a schedule the migration to Route Handlers changes how those endpoints are defined but not how they are triggered. Your existing cron expressions that point to those endpoints work without modification. If you need to review or update your cron schedule alongside the migration, our Cron Job Expression Generator lets you build, decode, and validate any cron expression with platform-specific formatting for Vercel, AWS, and GitHub Actions.
Step 7: Turbopack and Custom Webpack Configuration
If your project has custom Webpack configuration in next.config.ts, it will not apply when Turbopack is active. You have two options. First, migrate the configuration to Turbopack's equivalent API Turbopack's documentation lists direct equivalents for the most common Webpack customizations including aliases, loaders, and module resolution. Second, if you have complex Webpack setup that has no Turbopack equivalent yet, add the explicit fallback flag to next.config.ts:
// next.config.ts
const nextConfig = {
// Temporarily opt out of Turbopack for development
// until custom Webpack config is migrated
};
export default nextConfig;The 400% dev server speed improvement from Turbopack is real and worth the migration effort for most teams. Prioritize this over keeping legacy Webpack customizations that may not be necessary anymore.
Using AI Tools to Speed Up the Migration
Pages Router to App Router migration involves a lot of mechanical, repetitive transformations: converting data fetching patterns, adding 'use client' directives, rewriting layouts. These are exactly the tasks where AI coding tools produce reliable output on the first attempt, because the patterns are well-defined and consistent. If you use an AI IDE for this work, the quality of output depends heavily on how clearly you describe your stack and the specific transformation you need. Our AI Prompt Optimizer helps you structure those migration prompts to get production-quality output rather than boilerplate that needs significant rework.
Common Mistakes That Derail Next.js Migrations
Migrating everything at once: The biggest mistake is treating this as a big-bang rewrite instead of an incremental move. Next.js supports both routers simultaneously. Use that. Move one route per day if needed.
Forgetting to audit third-party packages: Some libraries depend on Pages Router internals. Before migrating, check that every package in your
package.jsonhas explicit App Router support documented. Authentication libraries and analytics packages are the most common offenders.Adding
'use client'to every component out of habit: Client Components lose the performance benefits of Server Components. Only add'use client'where you actually need browser APIs, state, or event handlers. Keep as much as possible as Server Components.Skipping the caching audit: The caching default inversion is silent your app will not throw errors, it will just make more network requests than before because previously cached calls are now dynamic. Measure your API call frequency before and after migration.
Not testing on Node.js 20 first: If you upgrade Next.js without upgrading Node.js, you will hit confusing compatibility errors that look like migration bugs but are actually runtime issues. Update Node.js before anything else.
Frequently Asked Questions
Do I have to migrate to the App Router to use Next.js 16?
No, Next.js 16 still runs both routers simultaneously and the Pages Router remains functional. However, all active development and new features from the Next.js team are focused exclusively on the App Router. The Pages Router is in maintenance mode. If you are building anything new in 2026, use the App Router. For existing projects, plan the migration rather than indefinitely postponing it.
What is the difference between Server Components and Client Components in Next.js App Router?
Server Components run only on the server. They can directly access databases, read files, and call APIs without any browser-side JavaScript. They render once and send the result as HTML. Client Components are marked with 'use client' and run in the browser. They support useState, useEffect, event handlers, and all browser APIs. The App Router defaults to Server Components you opt into client behavior only when you need it.
How long does a Pages Router to App Router migration take?
For a small project with under 20 pages, one to two days is realistic. For a mid-size application with 50 to 100 pages, one to two weeks of incremental migration is typical. Large enterprise codebases with complex layouts, authentication, and many third-party integrations may take four to eight weeks. The key factor is not page count but the complexity of your data fetching patterns and how many third-party libraries need App Router-compatible replacements.
Why did Next.js 16 reverse the caching defaults?
The original opt-out caching model in Next.js 13 caused widespread confusion. Developers would build pages that appeared to show dynamic data but were actually serving stale cached responses because a single fetch() call was implicitly cached. The number of support issues and production bugs caused by unexpected caching behavior was high enough that the team reversed the default. The opt-in model requires you to consciously choose caching, which eliminates the entire class of "why is my data stale" bugs.
What is the use cache directive and how does it replace unstable_cache?
The use cache directive is a first-class caching primitive in Next.js 16 that replaces the old unstable_cache() API. You add it to the top of a file, a function, or a component to mark that scope as cacheable. You combine it with cacheLife() to set expiry and cacheTag() for on-demand revalidation. The advantage over unstable_cache is that it integrates directly with React's component model and works predictably across Server Components, Server Actions, and Route Handlers without wrapper boilerplate.
Can I use the App Router and Pages Router in the same Next.js project?
Yes. This is the officially supported migration path. Routes in the app/ directory and routes in the pages/ directory both work simultaneously in the same project. The two routers use different conventions and cannot share the same route path, but they can coexist for as long as your migration takes. This means you can migrate one page per day without any user-facing disruption to the routes that are still on the Pages Router.
Continue Reading
View All HubLevel Up Your Workflow
Free professional tools mentioned in this article
Stripe & PayPal Fee Calculator
Calculate the exact Stripe and PayPal transaction fees for US and UK markets. A free developer tool to estimate SaaS payouts, merchant costs, and revenues.
Unix Timestamp Converter
Convert Unix timestamps to readable dates and back instantly. View the current epoch time, convert any timestamp, and see results in any timezone.
CSS to Tailwind CSS Converter
Convert legacy CSS to modern Tailwind CSS utility classes instantly. 100% secure, free, and runs entirely in your browser. Boost your core web vitals today.
JWT Decoder & Verifier
Decode, parse, and verify JWT (JSON Web Tokens) securely in your browser. Validate claims and debug authentication payloads instantly with zero server logs.




