Migrating from the Pages Router
AdvancedThe two routers run side by side, so migration is incremental and route by route. The hard part is not the syntax — it is that data fetching and layouts work on different principles.
Overview
A great deal of production Next is still on the Pages Router, and moving is a real project rather than a rename. The mechanics are gentle: `pages/` and `app/` coexist, `app/` wins where they overlap, and you can move one route at a time. What does not translate is the model — `getServerSideProps` has no equivalent because a component fetches its own data, `_app.js` becomes nested layouts, and any client-side data fetching you had should probably not be ported at all. This concept also covers the Next 14 to 15 upgrade, which is smaller but has one breaking change everybody hits.
The Translation Table
What each Pages concept becomes.
// pages/_app.js -> app/layout.tsx (root layout)
// pages/_document.js -> app/layout.tsx (html and body live there)
// pages/index.js -> app/page.tsx
// pages/problems/[slug].js -> app/problems/[slug]/page.tsx
// pages/api/x.js -> app/api/x/route.ts
// pages/404.js -> app/not-found.tsx
// pages/500.js -> app/error.tsx / global-error.tsx
// getServerSideProps -> just await in the component
export async function getServerSideProps({ params }) { // before
const problem = await getProblem(params.slug)
return { props: { problem } }
}
export default async function Page({ params }) { // after
const problem = await getProblem((await params).slug)
return <Article problem={problem} />
}
// getStaticProps + revalidate -> export const revalidate
// getStaticPaths -> generateStaticParams
// next/head -> the metadata export
// next/router -> next/navigation (different API!)
// router.query -> useParams / useSearchParams
// router.pathname-> usePathname
// router.events -> gone; derive from usePathname
// The import path is the trap: next/router silently does nothing
// useful in app/, and the error is not obvious.Migrating Incrementally
An order that keeps the site working throughout.
// Both routers coexist. app/ takes precedence for a conflicting path,
// so you cannot have pages/about.js and app/about/page.tsx at once.
// A working order:
// 1. Add app/layout.tsx — the root layout, alongside pages/_app.js.
// 2. Move ONE simple, static, low-traffic route. Learn the shape.
// 3. Move leaf routes before shared ones.
// 4. Convert API routes to handlers as their consumers move.
// 5. Move the highest-traffic routes last, with the most testing.
// 6. Delete pages/ only when it is empty.
// Do not port client-side fetching verbatim. A page with useEffect +
// useState + a loading flag usually becomes four lines in a server
// component — porting the old shape carries the complexity across
// for nothing.
// Shared components mostly work unchanged, but anything using state,
// effects or event handlers needs 'use client'. Add it at the leaves,
// not at the top of the tree (see the boundary concept).
// Two things that commonly break during migration:
// a global CSS import in _app.js -> move to the root layout
// a CSS-in-JS library -> needs a registry, or replacingUpgrading 14 to 15
Smaller, but with one change that touches every dynamic route.
npx @next/codemod@canary upgrade latest // does most of it
// 1. params and searchParams became Promises — the big one:
export default async function Page({ params }) {
const { slug } = await params // was: params.slug
}
// Same for cookies(), headers() and draftMode():
const cookieStore = await cookies()
// 2. Caching defaults flipped:
// fetch is no longer cached by default
// GET route handlers are no longer cached
// the client router cache is 0 for dynamic routes
// Anything that relied on the old defaults now hits your origin far
// more often. Add explicit cache/revalidate where you meant to cache.
// 3. React 19 is required, which brings useActionState (replacing
// useFormState) and the stable use() hook.
// Upgrade order that avoids a long-lived branch:
// read the release notes, run the codemod, fix the async params,
// then AUDIT every fetch for an explicit cache setting, then test
// against a production build.
// This codebase is on 14.2.x, so the async-params and caching notes
// throughout this track describe the change you will meet when it
// moves.Key Points to Remember
- 1pages/ and app/ coexist, so migration is route by route with app/ taking precedence on conflicts
- 2getServerSideProps has no equivalent — the component awaits its own data instead
- 3next/router must become next/navigation, and the API differs rather than just the path
- 4Do not port useEffect-based fetching; it usually collapses into a few lines in a server component
- 5The Next 15 upgrade makes params, searchParams, cookies and headers async, and flips the caching defaults
Interview Questions
Sign in to ask AriaHow would you migrate a large Pages Router app incrementally?
What replaces getServerSideProps in the App Router?
What are the breaking changes when upgrading from Next 14 to 15?
Ask Aria about Migrating from the Pages Router
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.