Migrating from the Pages Router to the App Router: An Incremental Checklist
- Author
- Vishal Maurya
- Published on
- Reading time
- 4 min read
Overview
A rewrite of every route at once is risky. Fortunately you do not need one: the pages/ and app/ directories can coexist in the same project, so you can migrate route by route. The main rule is that a given URL must be served by only one router, otherwise the build reports a conflict.
This checklist assumes a reasonably current Next.js version. Confirm requirements such as the minimum React and Node versions in the official upgrade guide.
1. Upgrade First, Migrate Second
Upgrade Next.js and React to a version that supports the App Router fully, and get the existing Pages Router app passing tests and building. Separating the upgrade from the migration makes regressions easier to attribute. Check the release notes for breaking changes, including changes to caching defaults and async request APIs between major versions.
2. Create the Root Layout
The app/layout.tsx replaces _app and _document. Move global CSS imports, fonts, and the html and body tags there:
// app/layout.tsx
import './globals.css';
import type { ReactNode } from 'react';
export const metadata = {
title: { default: 'My Site', template: '%s | My Site' },
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Providers that used to live in _app become a client component rendered from this layout, since layouts are Server Components.
3. Pick Low-Risk Routes First
Start with simple, mostly static pages such as an about page or a legal page. Build confidence in the layout, metadata, and styling before touching routes with heavy data or authentication.
Move the route by creating app/about/page.tsx and deleting pages/about.tsx in the same change. Leaving both produces a conflict.
4. Replace Data-Fetching Functions
The Pages Router data functions do not exist in the App Router. Components fetch data directly as async Server Components:
// Before: pages/posts/[id].tsx used getServerSideProps
// After: app/posts/[id]/page.tsx
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const res = await fetch(`https://api.example.com/posts/${id}`, { cache: 'no-store' });
const post: { title: string } = await res.json();
return <h1>{post.title}</h1>;
}
Rough mapping: getServerSideProps becomes a dynamic fetch, getStaticProps a cached or time-revalidated fetch, and getStaticPaths becomes generateStaticParams. Caching defaults for fetch differ between Next.js 14 and 15, so set cache or revalidate explicitly. In Next.js 14, params is not a Promise.
5. Move Head Tags to the Metadata API
Replace next/head with the metadata export or generateMetadata for dynamic values. Do this per route as you migrate, and verify titles, canonical URLs, and Open Graph tags in the rendered HTML.
6. Update Navigation Hooks
useRouter from next/router does not work in the App Router. Use hooks from next/navigation:
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
The App Router version has no query object and no router events. Read the path and search params through separate hooks. Shared components used by both routers may need an adapter during the transition.
7. Handle Client-Only Code
Anything using state, effects, or browser APIs needs "use client". A pragmatic first step is to move an existing page component into a client component and render it from a thin server page, then split out server work gradually.
8. Convert API Routes When Convenient
pages/api routes continue to work, so they are not a blocker. Convert them to Route Handlers (route.ts) later, exporting functions named after HTTP methods. Do not mix both for the same path.
9. Check Middleware, Redirects, and Auth
Middleware works across both routers. Review any logic that depended on Pages Router specifics, such as locale routing: internationalized routing configured in next.config.js is a Pages Router feature, and you will implement locale handling yourself in the App Router. Re-test redirects and authentication flows end to end.
10. Verify and Remove Leftovers
Before deleting _app and _document, confirm nothing still depends on them. Run the production build, compare rendered HTML for key pages, and check analytics, error monitoring, and SEO tags. Roll out gradually with feature flags or deploy previews if possible.
Conclusion
An incremental migration keeps the app shippable at every step. Upgrade first, move simple routes, replace data fetching and head management deliberately, and keep both routers working until the last route has moved.
If you plan a migration and want a route-by-route plan or hands-on help, I can assist. Contact me.