Home/Blog/frontend engineering
Back to all articles
frontend engineering#Next.js 15#App Router#React Server Components

Next.js Pages to App Router Migration Guide: Architecture, RSC & Server Actions

RB
Rajput BhavinArchitect
Founder, Rajput Bhavin Engineering
24 August 2026•11 min read

Quick Answer: Migrating to Next.js App Router#

⚡ Quick Answer / Executive Summary
Migrating from the Next.js Pages Router (/pages) to the modern App Router (/app) transitions your frontend architecture from client-heavy Single Page Applications (SPAs) to React Server Components (RSC). This migration replaces legacy getServerSideProps and getStaticProps with direct async Server Components, moves _app.js and _document.js into nested layout.tsx trees, replaces API routes with Route Handlers and Server Actions, and achieves up to 70% reduction in client-side JavaScript bundle sizes.

Why Migrate? Pages Router vs. App Router Architecture#

The App Router is not merely a syntactic update; it represents a complete paradigm shift in how React applications render and hydrate:

Dimension / MetricPages Router (/pages)App Router (/app)
Default RenderingClient Components (All JS sent to browser)Server Components by default (Zero client JS)
Data FetchinggetStaticProps / getServerSidePropsNative fetch with granular caching & async components
Layout SystemBrittle getLayout patterns, full re-rendersNested layouts with state preservation
MutationsCustom API routes + Axios/SWR fetchNative React Server Actions with optimistic updates
SEO & Schemanext/head (client-dependent)Static & dynamic generateMetadata API
Streaming UIHard to implementBuilt-in loading.tsx & React

5-Step Migration Strategy (Zero Downtime)#

At Rajput Bhavin Engineering, we implement an incremental migration strategy that allows /pages and /app to co-exist simultaneously during development:

Architecture Blueprint
System Flow
Step 1: Root Layout & Global Providers
  ├── Create app/layout.tsx (replaces _app.tsx and _document.tsx)
  └── Configure font optimization (next/font) and global CSS
       │
Step 2: Migrate Static Content & Marketing Pages
  ├── Convert marketing pages to pure React Server Components
  └── Implement generateMetadata() for SEO & schema markup
       │
Step 3: Migrate Dynamic Routes & Data Fetching
  ├── Replace getServerSideProps with async Server Components
  └── Wrap interactive client widgets in 'use client' boundaries
       │
Step 4: Modernize Data Mutations (Server Actions)
  ├── Replace legacy /api routes with type-safe Server Actions
  └── Implement useActionState & useOptimistic for instant UX
       │
Step 5: Cutover & Deprecate Legacy Pages
  ├── Remove redundant /pages directory files
  └── Verify 100/100 Core Web Vitals across all routes

Code Comparison: Data Fetching & Mutations#

#1. Legacy Pages Router Pattern (pages/products/[id].tsx)

TypeScript
Production Code
// Legacy: Requires complex wrapper and client-side bundle overhead
import { GetServerSideProps } from 'next';
import Head from 'next/head';

export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  const res = await fetch(`https://api.example.com/products/${params?.id}`);
  const product = await res.json();
  return { props: { product } };
};

export default function ProductPage({ product }: { product: any }) {
  return (
    <>
      <Head>
        <title>{product.name}</title>
      </Head>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
    </>
  );
}

#2. Modern App Router Pattern (app/products/[id]/page.tsx)

TypeScript
Production Code
// Modern: Clean async server component with zero client-side JS overhead
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const product = await getProduct(id);
  if (!product) return { title: 'Product Not Found' };

  return {
    title: `${product.name} | Rajput Bhavin Engineering`,
    description: product.description,
  };
}

export default async function ProductPage({ params }: Props) {
  const { id } = await params;
  const product = await getProduct(id);
  if (!product) notFound();

  return (
    <article className="p-8 max-w-4xl mx-auto">
      <h1 className="text-3xl font-black text-white">{product.name}</h1>
      <p className="text-lg text-foreground/70">${product.price}</p>
    </article>
  );
}

4 Common Migration Gotchas to Avoid#

  1. ✓Overusing 'use client': Placing 'use client' at the top of a page file turns the entire route tree into a client bundle. Keep 'use client' isolated to leaf interactive components (such as buttons, dropdowns, and modal dialogs).
  2. ✓Accessing Request Headers in Server Components: In Next.js 15, cookies() and headers() are asynchronous functions (const cookieStore = await cookies()).
  3. ✓Broken Global State Providers: Context providers (Redux, Zustand, ThemeProviders) must be wrapped inside dedicated client components ('use client') and passed as children to app/layout.tsx.
  4. ✓Caching Behavior in Next.js 15: Unlike Next.js 14 (which cached fetch by default), Next.js 15 sets fetch requests to no-store by default. You must explicitly specify fetch(url, { cache: 'force-cache' }) or next: { revalidate: 3600 }.

Modernize Your Web Architecture with Rajput Bhavin Engineering#

Migrating to Next.js 15 App Router unlocks dramatic performance gains, lower cloud infrastructure costs, and superior developer ergonomics.

Modern Web Engineering

Building a high-performance web experience?

Rajput Bhavin Engineering builds 100/100 Core Web Vitals websites, web applications, and headless e-commerce stores using Next.js 15, React 19, and Tailwind CSS.

RB

Rajput Bhavin

Systems Architect

Founder and Systems Architect at Rajput Bhavin Engineering in Gota, Ahmedabad. Specializes in high-concurrency cloud software, Next.js web applications, multi-tenant SaaS architecture, and autonomous AI systems.

Connect Now