Written for Next.js 16.x (current stable as of August 2026). Pages Router support status verified against Next.js 16 docs — not deprecated, no removal date announced.
Next.js shipped the App Router as the default in version 13, and most teams running a Pages Router app now have to decide whether to migrate, and if so, in what order. Waiting for a clean rewrite window rarely works, so the real decision is which routes to move first without breaking the ones you leave behind.
This article gives you two things: a decision framework for whether your app should move at all, and a risk-ordered sequence for doing it route by route if it should. You will also get a feature-by-feature list of what actually breaks, with the specific hook, export, or file convention involved.
If your pages/_app.js has grown into a dumping ground for shared layout state, and your dashboard fires eight independent client-side fetches on mount, those are the two signals that make this decision urgent.
In this guide:
- TL;DR: Which Router Should You Use?
- Why Next.js Shipped a Second Router
- Choose the Router Before You Move Code
- Compare the Route and Layout Models
- Replace Page-Level Data Fetching With Server Rendering
- Redraw the Server and Client Component Boundary
- What Breaks, and What Has No Replacement
- Migrate Loading, Errors, Metadata, and Mutations
- Execute a Risk-Ordered Migration Plan
- Frequently Asked Questions
- Get the Migration Staffed Right
TL;DR: Which Router Should You Use?
- Greenfield project: use the App Router. No migration cost, and it is the default in current Next.js.
- Actively developed app, tight deadline: migrate incrementally, starting with static routes. Do not pause the roadmap for a full rewrite.
- Legacy app in maintenance mode: stay on Pages Router. It still ships and is not deprecated.
- Heavy client-library dependence: audit first. If your CSS-in-JS runtime or component kit has no Server Component path, wait.
- Layout duplication or fetch waterfalls: migrate. Nested layouts and server-side fetching fix those directly.
- Always: keep URLs identical, set caching explicitly per route, and migrate shared-state routes as one batch.
Why Next.js Shipped a Second Router
React Server Components solve a specific problem: JavaScript that only runs to generate HTML was still shipping to the browser, inflating bundles and delaying interactivity. A Markdown renderer, a date formatting library, a syntax highlighter; none of these need to run in the browser, but under the Pages Router model, they shipped anyway.
Server Components run on the server and stream HTML directly. They can read databases and call APIs without exposing those calls to the client. The client receives HTML, not a JavaScript module.
The App Router is the routing model that makes Server Components the default. Without a new router, Next.js couldn’t change which components run where; the Pages Router’s architecture assumes all components are client-renderable. The new file conventions (layout.tsx, loading.tsx, error.tsx) are the surface area for Server Component composition.
The learning curve is real. The cache model is genuinely new. But the architectural problem it solves (shipping too much JavaScript to the browser) is real, too.
Choose the Router Before You Move Code
Understanding the Next.js App Router vs Pages Router differences starts with the rendering model, not folder structure. Both share the same framework but use different rendering models.
Pages Router maps files to routes and renders client-first, with data fetching declared at the page level, while App Router uses folder-based routing, renders Server Components by default, and supports nested layouts and streaming.
Here is the practical comparison:
| Area | Pages Router | App Router |
| Rendering model | Client Components by default; page-level SSR/SSG | React Server Components by default; ‘use client’ opts into the client bundle |
| Data fetching | getServerSideProps, getStaticProps, getStaticPaths exported from the page | async/await with fetch directly inside Server Components; generateStaticParams for dynamic paths |
| Layouts | _app.js and _document.js; remount-prone shared UI | layout.js per segment; nested layouts persist across navigation |
| Routing conventions | Every file in pages/ is a route | Folders define routes; page.tsx makes a segment public |
| Middleware | middleware.ts at project root | proxy.ts on the Node runtime (Next.js 16); middleware.ts is deprecated for Node runtime but still works for Edge Runtime use cases |
| Third-party support | Mature; nearly every React library works | Client-only libraries need a ‘use client’ wrapper; some still break |
| Learning curve | Low; one mental model | Higher; server/client boundary plus caching semantics |
When the Pages Router Is Still the Lower-Risk Choice
- Your app is mostly interactive. A trading UI or a canvas editor ends up with ‘use client’ at the top of the tree anyway. You keep the migration cost and lose the RSC payload savings.
- You depend on client-only libraries with no Server Component story. Older CSS-in-JS runtimes, some drag-and-drop kits, and legacy analytics wrappers fall here.
- Your app is in maintenance mode. Pages Router still ships in Next.js 16 and is not deprecated. No removal date has been announced. Rewriting stable routes buys nothing.
- Your team is small and shipping to a deadline. The caching model alone takes real time to internalize.
When the App Router Solves a Real Architecture Problem
- Layout duplication. If three route groups need three different shells and you fake it with conditionals in _app.js, nested layout.tsx files remove that logic entirely.
- Fetch waterfalls. A dashboard with 12 client components each calling useEffect + fetch serializes work behind hydration. Colocating those fetches in Server Components lets Next.js start them on the server and run them in parallel.
- Oversized client bundles. Markdown renderers, date libraries, and syntax highlighters stay on the server in Server Components and never ship to the browser.
- Slow time-to-first-byte on data-heavy pages. getServerSideProps blocks the whole response. Streaming with Suspense sends the shell first.
How Both Routers Coexist During Incremental Adoption
Next.js supports app/ and pages/ in the same project. Routing resolves per path, so app/dashboard/page.tsx and pages/settings.tsx both work in one deploy.
Two rules keep this sane:
- Never define the same path in both directories. You get a build-time conflict error, not a silent fallback.
- Navigation between routers is a full page load, not a client-side transition. Client state in a Pages Router provider resets when the user crosses into app/. Plan which routes share state and migrate those together.
Compare the Route and Layout Models
Both use file-based routing, but the rules for which file creates a URL differ. In pages/, a file is a route. In app/, a folder is a route segment, and only specific filenames are public.
Files That Create Routes in Each Directory
| Pages Router | App Router | URL |
| pages/index.js | app/page.tsx | / |
| pages/about.js | app/about/page.tsx | /about |
| pages/blog/[slug].js | app/blog/[slug]/page.tsx | /blog/hello |
| pages/api/users.js | app/api/users/route.ts | /api/users |
The useful side effect: any file in app/ that is not page, layout, route, loading, error, not-found, or template is private. You can put app/blog/components/PostCard.tsx next to the route it serves without creating a /blog/components URL. Under Pages Router, that same file becomes a broken route.
From _app.js and _document.js to layout.js
- _app.js becomes the root app/layout.tsx. It must return <html> and <body> tags itself.
- _document.js has no direct equivalent. Its job splits between the root layout markup and the Metadata API.
- Nested layout.tsx files replace conditional layout logic. Each layout wraps its segment’s children and does not remount on sibling navigation.
- Common failure: copying _document.js custom <Head> logic into app/layout.tsx and using next/head. next/head does not work in Server Components; use the metadata export.
// Before: pages/_app.tsx
import { SessionProvider } from 'next-auth/react'
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
)
}
// After: app/providers.tsx ('use client' wrapper)
'use client'
import { SessionProvider } from 'next-auth/react'
export function Providers({ children }) {
return <SessionProvider>{children}</SessionProvider>
}
// app/layout.tsx (stays a Server Component)
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
Dynamic, Catch-All, and Advanced Route Segments
- [id] works the same in both, but you read params from the params prop, not useRouter().query.
- […slug] and [[…slug]] keep the same syntax and semantics.
- (marketing) Route groups let you share a layout across /about and /contact without adding a URL segment. No Pages Router equivalent.
- @modal parallel routes and (.)photo intercepting routes enable modals that keep their own URL. Genuinely new capability, and also the part teams most often over-apply.
Where API Endpoints Live
- pages/api/users.js exports a default handler(req, res).
- app/api/users/route.ts exports named functions per method: GET, POST, PATCH, DELETE.
- Route Handlers use Web Request and Response objects. res.status(200).json(…) does not exist; you return Response.json(data) or NextResponse.json(data).
- Failure mode: copying a Pages API route into route.ts unchanged gives you a runtime error on res.status, because res is undefined.
// Before: pages/api/users.ts
export default function handler(req, res) {
if (req.method === 'GET') {
res.status(200).json({ users: [] })
}
}
// After: app/api/users/route.ts
export async function GET() {
return Response.json({ users: [] })
}
export async function POST(request: Request) {
const body = await request.json()
// handle POST
return Response.json({ created: true }, { status: 201 })
}
You can leave pages/api/ in place indefinitely. It keeps working alongside app/, which makes API routes the safest thing to defer.
Replace Page-Level Data Fetching With Server Rendering
This is the largest behavioral change. Pages Router data fetching is a page-level export that runs once per request or per build. App Router data fetching is a function call inside any Server Component, at any depth.
Mapping getStaticProps and getStaticPaths to App Router Patterns
Before:
- getStaticProps returns { props } and revalidate
- getStaticPaths returns { paths, fallback }
After:
- The page component becomes async and awaits fetch directly. No props return shape.
- getStaticPaths becomes generateStaticParams, which returns a plain array like [{ slug: ‘hello’ }].
- fallback: ‘blocking’ maps to the default behavior: unknown params render on demand. fallback: false maps to export const dynamicParams = false.
- revalidate moves to either export const revalidate = 60 on the segment or fetch(url, { next: { revalidate: 60 } }) per request.
// Before: pages/blog/[slug].tsx
export async function getStaticPaths() {
const posts = await getPosts()
return { paths: posts.map(p => ({ params: { slug: p.slug } })), fallback: false }
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug)
return { props: { post }, revalidate: 60 }
}
export default function BlogPost({ post }) {
return <Article post={post} />
}
// After: app/blog/[slug]/page.tsx
export const dynamicParams = false
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(p => ({ slug: p.slug }))
}
export const revalidate = 60
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return <Article post={post} />
}
Watch for: getStaticProps returning { notFound: true }. In App Router, you call notFound() from next/navigation, which throws and renders the nearest not-found.tsx.
Replacing getServerSideProps With Async Server Components
- Delete the export, make the component async, and await your data inside it.
- context.req and context.res are gone. Read cookies with cookies() and headers with headers() from next/headers.
- In current Next.js versions, params and searchParams are Promises. You must await params before reading params.slug. Forgetting this is a common upgrade error.
- Calling cookies() or headers() opts the whole segment into dynamic rendering. If you expected a static page and got dynamic, this is usually why.
// Before: pages/dashboard.tsx
export async function getServerSideProps(context) {
const data = await fetchDashboard(context.req.cookies.token)
return { props: { data } }
}
export default function Dashboard({ data }) {
return <DashboardView data={data} />
}
// After: app/dashboard/page.tsx
import { cookies } from 'next/headers'
export default async function Dashboard() {
const token = (await cookies()).get('token')?.value
const data = await fetchDashboard(token)
return <DashboardView data={data} />
}
Caching, Revalidation, and Static Generation Decisions
This is where teams get burned. App Router caches at multiple layers, and data that looked fresh under getServerSideProps can go stale.
- Data Cache: persists fetch results across requests and deploys unless you set cache: ‘no-store’ or a revalidate window.
- Full Route Cache: stores the rendered HTML for static segments.
- Router Cache: caches RSC payloads client-side during a session, so a back navigation may show older data.
- On-demand invalidation: revalidatePath(‘/blog’) and revalidateTag(‘posts’) replace the Pages Router pattern of hitting a revalidation API route.
Self-hosted vs Vercel caching: on Vercel, the Data Cache and Full Route Cache are managed infrastructure you don’t configure. Self-hosted, you need to configure cacheHandler in next.config.js to point to a custom cache implementation (Redis, file system, etc.) for ISR and route handler caching to persist across restarts and deploys.
In Next.js 16, note the naming distinction: cacheHandler (singular) handles server cache operations for ISR and route handlers; cacheHandlers (plural, new in Next.js 16) handles ‘use cache’ directive caching separately. Missing this distinction when upgrading from 15 to 16 is a real production gotcha.
Practical rule: audit every former getServerSideProps route and decide explicitly between cache: ‘no-store’ and a revalidate value. Silence here means you inherit a default you did not choose.
Avoiding Client-Side Fetching Waterfalls
The concrete win looks like this. A settings page with a user card, a billing widget, and an activity feed, each fetching in its own useEffect, cannot start any request until JavaScript loads and hydrates.
Move those three fetches into three Server Components and Next.js kicks them off server-side. Sibling components fetch in parallel. To keep parallelism inside one component, use Promise.all rather than sequential await calls, since two awaits in a row still serialize.
Wrap the slow one in <Suspense> and the fast two render immediately.
Redraw the Server and Client Component Boundary
The server/client boundary is the concept that decides whether your migration goes smoothly. Everything in app/ is a Server Component until a file says otherwise.
What the use client Directive Changes
- ‘use client’ at the top of a file marks a bundle boundary. That file and everything it imports ship to the browser.
- The directive is inherited downward. A ‘use client’ layout makes every child component a Client Component, which quietly erases your bundle savings.
- Server Components cannot use useState, useEffect, useContext, event handlers, or browser APIs. Using them produces a build error naming the hook.
- You can pass serializable props from Server to Client Components. You cannot pass functions or class instances; you get a serialization error.
- Rule that matters: push ‘use client’ as deep as possible. A search input needs it. The page wrapping it does not.
Replacing next/router and useRouter
- Import useRouter from next/navigation, not next/router. The old import throws a runtime error inside app/.
- router.query is gone. Use the params prop in Server Components or useParams() and useSearchParams() in Client Components.
- router.pathname is gone. Use usePathname().
- router.events has no replacement. Route-change listeners for analytics or scroll restoration must be rebuilt with usePathname() in an effect.
- router.push and router.replace survive. shallow: true does not.
// Before: any component in pages/
import { useRouter } from 'next/router'
export function Nav() {
const router = useRouter()
return (
<button onClick={() => router.push('/dashboard')}>
Go to {router.pathname}
</button>
)
}
// After: any component in app/ (must be 'use client')
'use client'
import { useRouter, usePathname } from 'next/navigation'
export function Nav() {
const router = useRouter()
const pathname = usePathname()
return (
<button onClick={() => router.push('/dashboard')}>
Go to {pathname}
</button>
)
}
That router.events gap breaks more analytics setups than any other single change.
Moving State, Effects, Context, and Browser APIs
- A global provider in _app.js moves into a small ‘use client’ component rendered inside app/layout.tsx. Do not add ‘use client’ to the layout file itself.
- Context still works, but only below a ‘use client’ boundary. Server Components cannot read it.
- Redux, Zustand, and TanStack Query all work behind a client provider. TanStack Query is often still worth keeping for mutations and polling.
- window, localStorage, and document throw during server render. Guard them in useEffect, or move the component into a Client Component and gate it there with dynamic(…, { ssr: false }). That option isn’t allowed in Server Components—using dynamic with { ssr: false } in a Server Component produces a build error.
- Client-heavy dashboards on older React versions usually hit the wall at shared layout state, since a Server Component layout cannot hold client context.
Read more: Best Platforms to Hire React Developers in 2026
Auditing Third-Party Libraries and Client Bundles
Before you migrate a route, check each dependency:
- CSS-in-JS: styled-components and Emotion are runtime libraries that relied on _document.js for style injection; they need a Client Component provider wrapper in App Router and have varying levels of official App Router support as of mid-2026 (check each library’s current docs). MUI and Chakra UI ship their own App Router adapters; vanilla-extract and Linaria are zero-runtime and work fine without changes.
- Auth: Auth.js (NextAuth v5), Clerk, Supabase Auth, and Lucia all ship App Router support as of 2026, but session-reading differs between Server Components, Route Handlers, and middleware in each library. Read your version’s docs specifically — many Stack Overflow answers and blog posts still show the Pages Router API.
- Analytics: anything hooked to router.events needs rewriting.
- Component kits: dnd-kit requires Client Component wrapping for its hooks. Most component libraries (Radix UI, shadcn/ui) are already compatible or have explicit guidance. Check whether a library exports hooks — if it does, it needs ‘use client’.
The table below shows verified compatibility status as of August 2026, Next.js 16.
| Library | Status | Notes |
| Auth.js / NextAuth v5 | Works | Has dedicated App Router adapter |
| Clerk | Works | Full App Router support |
| Supabase Auth | Works | Server Component helpers available |
| Lucia | Works | Designed for Server Components |
| styled-components | Needs wrapper | Client Component provider required |
| Emotion | Needs wrapper | Check library version for adapter |
| MUI | Works (adapter) | Use official App Router setup guide |
| Chakra UI | Works (adapter) | v3+ supports App Router |
| vanilla-extract | Works | Zero-runtime, no changes needed |
| Linaria | Works | Zero-runtime, no changes needed |
| dnd-kit | Needs wrapper | All hooks require Client Components |
Auth note: session-reading patterns that previously used middleware.ts need updating to proxy.ts in Next.js 16 for Node runtime deployments. Each library’s current docs show the updated pattern — the API call is the same; only the filename and export name change.
Run next build and read the per-route First Load JS numbers before and after. If they did not drop, your ‘use client’ boundaries are too high in the tree.
What Breaks, and What Has No Replacement
Not all Pages Router APIs have a migration path. The distinction matters: some gaps require architectural decisions, not just a syntax update.
No Replacement in the App Router
These APIs are gone with no direct equivalent. Work around them or rebuild the behavior from scratch:
- router.events — the most common breakage. Route-change listeners for analytics, scroll restoration, and progress bars must be rebuilt using usePathname() in a useEffect. There is no event emitter. See the “Replacing next/router” section above.
- Built-in i18n routing — the i18n config in next.config.js that automatically prefixes routes with /en/ or /fr/ does not work in the App Router. Use a third-party library (next-intl, next-i18next) or implement routing middleware yourself.
- next/script with strategy=”worker” — the Partytown-based worker strategy is not supported in the App Router. Remove it or replace it with a different third-party script isolation approach.
Changed API With a Migration Path
These exist in the App Router but look different enough to cause build or runtime errors if you copy code unchanged:
- next/compat/router — the official escape hatch for sharing components between both routers. If you’re running an incremental migration and a component needs to work in both pages/ and app/, import useRouter from next/compat/router rather than from either router directly. The article’s incremental migration thesis depends on this, but most migration checklists omit it.
- next/font replacing inlined font CSS — use the built-in-next-font codemod (npx @next/codemod@latest built-in-next-font) to migrate @next/font imports automatically.
- draftMode() replacing Preview Mode — getStaticProps’s preview and previewData context values don’t exist in Server Components. Import draftMode from next/headers instead.
- getInitialProps — if you used this in _app.js, it moves into Server Components. Unlike getServerSideProps, there’s no one-to-one replacement; restructure as a top-level async Server Component fetch.
- next/script event handlers (onLoad, onReady, onError) — these fail silently in Server Components. Move next/script with these handlers into Client Components. beforeInteractive scripts move to the root layout.
Migrate Loading, Errors, Metadata, and Mutations
The App Router replaces four patterns you hand-rolled under the Pages Router with file conventions. These are the easiest wins in the migration and a good place to build team confidence.
Using loading.tsx and Suspense for Streaming UI
- loading.tsx in a segment wraps that segment’s page.tsx in a Suspense boundary automatically. No manual isLoading state.
- For finer control, wrap individual slow components in <Suspense fallback={<Skeleton />}> inside the page.
- Streaming sends the shell and layout first, then fills in each boundary as data resolves. That improves perceived speed on a report page waiting on a slow query.
- Caveat: streaming only helps if the segment is dynamic. A fully static page has nothing to stream.
Replacing Manual Error Handling With error.tsx and not-found
- error.tsx is a client-side error boundary for its segment. It must start with ‘use client’ and receives error plus a reset() function.
- It does not catch errors thrown in the layout at the same level. Put error.tsx one level up or add global-error.tsx for root failures.
- notFound() from next/navigation renders the nearest not-found.tsx. This replaces { notFound: true } returns.
- pages/404.js and pages/500.js keep working for whatever still lives in pages/.
Moving SEO Configuration to the Metadata API
- Replace next/head with either a static export const metadata = { title, description } or an async generateMetadata({ params }) function.
- next/head inside a Server Component silently does nothing. That means missing meta tags in production with no build error, which is a quiet SEO regression.
- generateMetadata shares the Data Cache with your page, so fetching the same record twice does not cost two requests.
- Add app/sitemap.ts and app/robots.ts to replace hand-built route handlers.
- Do not change URLs during migration. Slug or trailing-slash drift causes 404s and deindexing. Keep paths identical and verify with a crawl diff.
// Before: pages/about.tsx
import Head from 'next/head'
export default function About() {
return (
<>
<Head>
<title>About Us</title>
<meta name="description" content="Learn about our team." />
</Head>
<main>...</main>
</>
)
}
// After: app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn about our team.',
}
export default function About() {
return <main>...</main>
}
// For dynamic metadata:
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return { title: post.title, description: post.excerpt }
}
Assessing Server Actions and Route Handlers
Server Actions let a form call a server function directly, with no API route in between.
Worth adopting when:
- Your mutation is a form post that revalidates a page, since revalidatePath right after the write is clean.
- You want progressive enhancement and less client fetch code.
Skip for now when:
- The endpoint is a public API surface consumed by a mobile app or third party. Use a Route Handler.
- You need fine-grained rate limiting, custom status codes, or webhook signature checks. Route Handlers give you the raw Request.
Server Actions aren’t required for the migration. You can move every route to app/ and keep calling your existing pages/api/ endpoints.
Execute a Risk-Ordered Migration Plan
Migrate in order of blast radius, starting with the lowest. Both directories run side by side, so there is no reason to attempt a big-bang cutover.
Inventory Route Dependencies and Define Test Coverage
Step 1. Build a spreadsheet of every route with four columns: data fetching method, client-only libraries used, shared context consumed, and traffic volume.
Step 2. Sort by traffic ascending and dependency count ascending. That ordering is your migration queue.
Step 3. Add end-to-end tests to your top-traffic routes before you touch anything. Without them, you cannot tell a caching change from a rendering bug.
Step 4. Enable the App Router ESLint rules so bad imports like next/router inside app/ fail in CI, not in review.
Codemods handle the mechanical parts. Run them before doing anything manually:
# Run all applicable codemods for your version in one pass
npx @next/codemod@latest .
# Or run specific codemods individually:
# Migrate async dynamic APIs (cookies, headers, params, searchParams)
npx @next/codemod@latest next-async-request-api .
# Remove <a> tags from inside <Link> components
npx @next/codemod@latest new-link .
# Migrate @next/font to built-in next/font
npx @next/codemod@latest built-in-next-font .
AI-assisted refactoring tools help with the repetitive ‘use client’ placement pass. Neither decides your caching strategy, where the real judgment lives.
Move Static, Isolated Routes First
Step 5. Create app/layout.tsx with your <html> and <body> shell. Add nothing else yet.
Step 6. Migrate marketing pages, docs, and legal pages. These use getStaticProps and have no client state, so the conversion is mostly deleting an export.
Step 7. Migrate one dynamic static route, such as a blog post page, to exercise generateStaticParams and revalidate end-to-end.
Step 8. Verify with next build output. Confirm the routes still render as static and that First Load JS dropped.
Migrate Shared Layouts and Interactive Routes Deliberately
Step 9. Port _app.js providers into a single ‘use client’ provider component inside the root layout. Keep the layout itself a Server Component.
Step 10. Move authenticated routes next, one group at a time. Update your auth library to its App Router API and test session reads in Server Components, Route Handlers, and proxy.ts separately. Note: middleware.ts is deprecated on the Node runtime in Next.js 16; rename to proxy.ts and update the exported function name from middleware to proxy if you use it for auth redirects or session checks.
Step 11. Migrate high-interaction routes last. Convert the data fetching to Server Components and keep interactive leaves as small Client Components.
Step 12. Group routes that share client state into the same migration batch. Splitting them across routers means state resets on full-page load between app/ and pages/.
Budget honestly here. A React engineer new to Server Components typically needs a couple of weeks of real work to stop guessing at ‘use client’ placement and caching behavior.
On a small team, that overlaps with your feature roadmap, so plan the migration batches around whoever on the team already has Server Component experience, not around whoever has the most free calendar time.
Read more: Best Platforms to Hire Next.js Developers in 2026
Validate Deployments, Caching, and Rollback Paths
Step 13. Ship behind a preview deployment and compare against production on the same routes: response headers, cache status, meta tags, and Core Web Vitals.
Step 14. Check every former getServerSideProps route for stale data. Confirm no-store or an explicit revalidate on each.
Step 15. Keep the old pages/ file in git history and know your revert path. Because routing resolves per path, rolling back one route means restoring one file and redeploying.
Step 16. Watch 404 rates and Search Console coverage for a full crawl cycle after each batch. URL drift shows up there before it shows up in traffic.
Frequently Asked Questions
Is the Pages Router deprecated?
No. Pages Router still ships in Next.js 16. It ships; it is not deprecated, and no removal date has been announced. The official docs describe it as still supported and improving, but the App Router is clearly where new features land first. Teams in maintenance mode or shipping to a tight deadline can stay on it without technical risk.
Can I use App Router and Pages Router in the same project?
Yes. Next.js resolves routing per path, so app/dashboard/page.tsx and pages/settings.tsx can both exist in the same deploy. You cannot define the same path in both directories, and navigation between the two routers triggers a full page load rather than a client-side transition.
How long does an App Router migration take?
It depends on route count and how deep your client-side dependencies run, not on a fixed timeline. A React engineer new to Server Components typically needs a couple of weeks of real work before they stop guessing at ‘use client’ placement and caching behavior. Migrating one static, isolated route first is faster than migrating a route with shared layout state.
Do I need to rewrite my whole app to adopt the App Router?
No. Both directories coexist, so you migrate route by route. Start with static, isolated pages like marketing or docs, then move authenticated routes, and save high-interaction routes for last. You don’t need to convert pages/api/ routes at all; they keep working alongside app/.
Is the App Router faster than the Pages Router?
It depends on what you are measuring. Server Components can cut client bundle size significantly since markdown renderers, date libraries, and syntax highlighters never ship to the browser. But the App Router adds caching layers (Data Cache, Full Route Cache, Router Cache) that the Pages Router doesn’t have, and some teams report slower Time to First Byte on certain routes until they configure those caches correctly. Raw speed comparisons without a specific route and cache configuration are not meaningful.
What breaks first when migrating to the App Router?
Three things surface immediately: useRouter imported from next/router throws a runtime error inside app/, getServerSideProps and getStaticProps exports do not work in Server Components, and any library relying on router.events for analytics or scroll tracking loses that hook with no direct replacement.
Should a new Next.js project use the App Router or Pages Router?
Use the App Router for any greenfield project. It is the current default, carries no migration cost, and is where new Next.js features ship first. Pages Router only makes sense for a new project if the team is already deeply familiar with it and needs to ship on a tight deadline with zero learning curve.
Can I migrate gradually without breaking production?
Yes, that is the intended path. Ship each migrated route behind a preview deployment first, compare response headers and cache status against production, and keep the old pages/ file in git history so rolling back one route means restoring one file and redeploying.
Get the Migration Staffed Right
None of the steps above are hard to understand. They take time to execute correctly, and that time gets expensive fast if the engineer doing the work is learning Server Component boundaries and caching semantics on your production app.
If this migration is on your roadmap and you are short on engineers who have already done it, Arc matches companies with vetted Next.js developers who have shipped App Router migrations in production.
Use it to fill the gap directly for the migration itself, or to check whether your current team already has the skill coverage for the routes you’d consider highest risk before you start.








