arrow_backTech Blog
code
codeFrontend2026-03-20·8 min read

Migrating from Pages Router to App Router: A Practical Guide

A complete record of migrating a mid-sized Next.js project from Pages Router to App Router, covering route restructuring, data fetching patterns, layout strategies, and common pitfalls.

Next.jsReactApp Router

Why Migrate?

App Router is the future of Next.js, offering better layout systems, Server Components support, and more granular data fetching control.

Migration isn't about chasing trends — it's about gaining better architecture: nested layouts, streaming rendering, and smaller client bundles.

Our project is a mid-sized B2B admin dashboard with ~40 pages. Under Pages Router, global state and layout management was increasingly painful: every page needed manual Layout wrapping, data fetching logic scattered across getServerSideProps, hard to reuse.

After two weeks of evaluation and one week of implementation, we completed the migration. Here are the key steps and lessons learned.

Migration Steps

1. Route Restructuring

Gradually move files from `pages/` to `app/` directory, using `page.tsx` and `layout.tsx` to replace page components.

The strategy was **incremental** — Next.js allows `pages/` and `app/` to coexist. We migrated module by module, ensuring every step was reversible.

  • First batchStatic pages (About, Help, etc.) — lowest risk
  • Second batchList pages — involving data fetching pattern changes
  • Third batchComplex form pages — heavy client-side interaction
  • 💡

    Start with the simplest pages to validate the build pipeline. Confirm middleware, env vars, and third-party libs work correctly before tackling complex pages.

    2. Data Fetching

    Shift from `getServerSideProps` / `getStaticProps` to async Server Components that fetch data directly.

    This is the biggest mental model shift. In Pages Router, data fetching and UI rendering are separate; in App Router, components themselves are the data fetching boundary.

  • Server ComponentDirectly await fetch or database queries — no API middleware needed
  • Parallel requestsIndependent data sources can fetch in parallel with Suspense for streaming
  • Cache controlPrecise caching via fetch's cache and revalidate options
  • The most pleasant surprise: removing getServerSideProps dramatically improved readability — fetch data right where you use it, no prop drilling through layers.

    3. Client Components

    Add `"use client"` directive to interactive components, keeping as much of the component tree as Server Components.

    Key principle: **Keep "use client" boundaries as small as possible**. Don't mark entire pages as client — push interactive logic to the smallest leaf components.

  • Form componentsNeed useState, event handlers → Client Component
  • Data displayPure display tables, cards → keep as Server Component
  • Mixed scenariosComposition pattern — Server Component fetches data, passes via children/props to Client Component

  • Pitfalls

    params Became a Promise

    In App Router, dynamic route params and searchParams became Promises in the latest versions — requires await. Easy to miss, but TypeScript types will catch it.

    Metadata API

    SEO config migrated from Head component to generateMetadata function. The upside: you can async fetch data for dynamic meta tags, like generating OG image descriptions from article content.

    CSS-in-JS Compatibility

    Some CSS-in-JS libraries (like styled-components) don't work in Server Components — need Client Component wrappers. We ultimately switched to Tailwind CSS, completely avoiding this issue.

    ⚠️

    If your project heavily relies on CSS-in-JS, verify RSC compatibility before migrating. This could be the biggest migration blocker.

    Third-party Library Compatibility

    Common libraries (date-fns, lodash) work fine in Server Components, but anything depending on browser APIs (chart libs, editors) must be used in Client Components.


    Performance Comparison

    We ran detailed benchmarks before and after:

  • Initial JS bundle287KB → 198KB (-31%)
  • LCP (Largest Contentful Paint)2.4s → 1.6s
  • TTI (Time to Interactive)3.1s → 2.2s
  • Build timeRoughly equal (faster with Turbopack in dev)
  • The most significant improvement came from Server Components — display-only components no longer ship JavaScript to the client, dramatically reducing bundle size.

    Results

    ~30% improvement in page load performance, cleaner code structure, and easier layout reuse. While migration has a learning curve, it's a worthwhile long-term investment.

    💡

    If your project is still on Pages Router, consider using App Router for new features and incrementally migrating existing pages. No need to do everything at once.

    code