<!-- StarterPick AI-readable guide source -->
<!-- Canonical: https://starterpick.com/guides/how-to-migrate-pages-router-to-app-router-2026 -->
<!-- Raw Markdown: https://starterpick.com/guides/how-to-migrate-pages-router-to-app-router-2026/raw.md -->
<!-- Source path: content/guides/how-to-migrate-pages-router-to-app-router-2026.mdx -->

---
title: "Migrate Next.js Pages Router to App Router 2026"
description: "Practical guide to migrating a Next.js SaaS boilerplate from Pages Router to App Router in 2026. Incremental migration, auth, data fetching, gotchas covered."
date: "2026-03-09"
author: "StarterPick Team"
tags: ["nextjs", "app-router", "pages-router", "migration", "saas-boilerplate", "2026"]
tier: 1
---

## TL;DR

**Migrate incrementally — App Router and Pages Router coexist in the same Next.js app.** Start by moving the root layout, then migrate one page at a time. The biggest changes: data fetching (no `getServerSideProps` → Server Components with `async`), auth (session in `cookies()` vs `getServerSideProps` context), and layouts (no `_app.tsx` → `layout.tsx` with nested providers).

## Key Takeaways

- **Incremental migration**: Pages Router pages still work while you migrate to App Router
- **Server Components**: `async function Page()` replaces `getServerSideProps`
- **Client Components**: Add `'use client'` only when needed (event handlers, hooks, state)
- **Auth migration**: `getSession(req)` → `auth()` from Auth.js/Clerk in Server Components
- **Layouts**: Nested `layout.tsx` files replace global `_app.tsx` + per-page layout pattern

---

## Why Migrate Now?

Most SaaS boilerplates launched before 2023 used the Pages Router. In 2026, the App Router is the standard — it ships with better caching, streaming/Suspense support, server-side components that dramatically reduce client bundle size, and nested layouts that eliminate a lot of prop-drilling.

Practically, if your boilerplate is Pages Router-based, you're missing out on:
- **Smaller client bundles**: Data fetching components are server-side by default — no JavaScript shipped to the browser for those components
- **Nested layouts**: Route groups with shared layouts without the `_app.tsx` workaround
- **Server Actions**: Form mutations without separate API routes
- **Parallel routes and intercepting routes**: Complex UX patterns (modals, drawers, side-by-side views) that were painful in Pages Router

The migration is straightforward for simple SaaS apps. The complexity scales with: number of pages, complexity of auth setup, use of context providers that wrap the whole app, and patterns like `getStaticProps` with ISR.

---

## When to Migrate vs When to Wait

**Migrate now if:**
- App has fewer than 30 pages
- Auth is NextAuth (Auth.js) — the migration path is well-documented
- No complex `getStaticProps` with ISR patterns
- Your team can allocate 1–2 focused sprints

**Wait to migrate if:**
- Your app has 100+ pages and a migration would take weeks of dedicated time
- You're mid-feature and can't afford the context switch
- You're using Pages Router-specific features (custom `_document.tsx`, complex `getStaticPaths`) that don't have a clean App Router equivalent

For most early-stage SaaS apps, the migration is worth doing before the codebase gets large.

---

## Step 1: Create App Directory Structure

```
src/
├── app/                    ← New App Router
│   ├── layout.tsx          ← Root layout (replaces _app.tsx)
│   ├── (auth)/             ← Route group (no URL segment)
│   │   ├── login/
│   │   │   └── page.tsx
│   │   └── signup/
│   │       └── page.tsx
│   ├── (dashboard)/        ← Protected route group
│   │   ├── layout.tsx      ← Dashboard layout with sidebar
│   │   └── dashboard/
│   │       └── page.tsx
│   └── api/                ← API routes
│       └── ...
├── pages/                  ← Old Pages Router (still works!)
│   ├── old-page.tsx        ← Pages Router pages still render
│   └── api/
│       └── legacy-api.ts
```

The coexistence of `app/` and `pages/` is not just a temporary workaround — Next.js officially supports running both simultaneously. You can migrate page by page, deploying at each step, with no big-bang migration required.

---

## Step 2: Root Layout (replaces _app.tsx)

The biggest conceptual shift: `_app.tsx` handled global providers and shared layout. In App Router, this is split into `app/layout.tsx` (root layout) and nested `layout.tsx` files per route group.

```tsx
// app/layout.tsx:
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/toaster';

const inter = Inter({ subsets: ['latin'] });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
          {children}
          <Toaster />
        </ThemeProvider>
      </body>
    </html>
  );
}
```

**Important:** All context providers (ThemeProvider, ReactQueryClientProvider, ClerkProvider, etc.) must be Client Components. If they aren't already, wrap them in a Client Component:

```tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

// Then in app/layout.tsx:
import { Providers } from './providers';
// ... wrap {children} with <Providers>
```

---

## Step 3: Migrate Data Fetching

This is the most impactful change — `getServerSideProps` is replaced by `async` Server Components. The result is significantly less code.

```typescript
// BEFORE — Pages Router getServerSideProps:
export async function getServerSideProps(context: GetServerSidePropsContext) {
  const session = await getSession(context);
  if (!session) return { redirect: { destination: '/login', permanent: false } };

  const user = await db.user.findUnique({ where: { id: session.user.id } });
  const projects = await db.project.findMany({ where: { userId: session.user.id } });

  return {
    props: {
      user: JSON.parse(JSON.stringify(user)),  // serialize dates
      projects: JSON.parse(JSON.stringify(projects)),
    },
  };
}

export default function DashboardPage({ user, projects }: Props) {
  return <Dashboard user={user} projects={projects} />;
}
```

```tsx
// AFTER — App Router Server Component:
import { auth } from '@/lib/auth';  // Auth.js or Clerk
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth();
  if (!session) redirect('/login');

  // Direct DB access in component — no getServerSideProps:
  const [user, projects] = await Promise.all([
    db.user.findUnique({ where: { id: session.user.id } }),
    db.project.findMany({ where: { userId: session.user.id } }),
  ]);

  return <Dashboard user={user!} projects={projects} />;
}
```

Key improvements: no date serialization hacks, parallel data fetching with `Promise.all`, auth check inline, no prop threading.

---

## Step 4: Auth Migration

Auth.js (formerly NextAuth.js) v5 changed its API significantly for App Router. If you're on NextAuth v4 (Pages Router era), the migration path is:

```typescript
// BEFORE — Pages Router middleware:
import { withAuth } from 'next-auth/middleware';

export default withAuth({
  pages: { signIn: '/login' },
});

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
};
```

```typescript
// AFTER — App Router middleware (Auth.js v5):
import { auth } from '@/lib/auth';
import { NextRequest } from 'next/server';

export default auth((req) => {
  if (!req.auth) {
    return Response.redirect(new URL('/login', req.url));
  }
});

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|login|signup).*)'],
};
```

For Clerk users, the migration is simpler — Clerk has first-class App Router support with `clerkMiddleware` and the `auth()` helper for Server Components.

---

## Step 5: Client Components

The default in App Router is Server Components. Only add `'use client'` when you actually need client-side features: event handlers, hooks, browser APIs, or third-party libraries that require a browser context.

```tsx
// Server Component (default — no 'use client'):
// Can fetch data, access server resources, no hooks
export default async function ProductList() {
  const products = await db.product.findMany();
  return (
    <ul>
      {products.map(p => (
        <ProductItem key={p.id} product={p} />
      ))}
    </ul>
  );
}

// Client Component (only when needed):
'use client';
import { useState } from 'react';

// ProductItem needs onClick — must be client:
export function ProductItem({ product }: { product: Product }) {
  const [expanded, setExpanded] = useState(false);
  return (
    <li onClick={() => setExpanded(!expanded)}>
      {product.name}
      {expanded && <p>{product.description}</p>}
    </li>
  );
}
```

The pattern is: push `'use client'` as far down the component tree as possible. A page component that fetches data should be a Server Component; the interactive parts (buttons, dropdowns, forms) should be Client Components that receive data as props.

---

## Common Migration Gotchas

**1. Context providers must be Client Components.** ThemeProvider, SessionProvider, QueryClientProvider — all require `'use client'`. Wrap them in a single `Providers` client component as shown above.

**2. No serialize/deserialize needed.** In Pages Router, you had to `JSON.parse(JSON.stringify(data))` to serialize dates and non-plain objects. In App Router, you pass DB objects directly as props from Server Components.

**3. Cookies and headers in App Router.** Use `import { cookies, headers } from 'next/headers'` in Server Components. This doesn't work in Client Components.

**4. Dynamic routes change shape.** Pages Router: `pages/posts/[id].tsx`. App Router: `app/posts/[id]/page.tsx`. The `params` are now a prop on the page component.

**5. API routes stay mostly the same.** You can keep them in `pages/api/` — both work. But if you want them to access Server Component patterns (`cookies()`, etc.), move them to `app/api/`.

**6. `getStaticProps` / ISR changes.** Static generation moves to `export const revalidate = 3600` at the segment level and `fetch` with cache options: `fetch(url, { next: { revalidate: 3600 } })`.

---

## Incremental Migration Checklist

**Week 1: Foundation**
- [ ] Create `app/` directory
- [ ] Move `_app.tsx` logic to `app/layout.tsx`
- [ ] Wrap providers in a Client Component
- [ ] Keep all `pages/` files — they still work
- [ ] Deploy and verify nothing broke

**Week 2: High-Traffic Pages**
- [ ] Migrate dashboard/home page first
- [ ] Convert `getServerSideProps` → async Server Components
- [ ] Update auth middleware to Auth.js v5 / Clerk pattern
- [ ] Test auth flows thoroughly

**Week 3: Remaining Pages**
- [ ] Migrate remaining pages one by one
- [ ] Move API routes to `app/api/` if needed
- [ ] Remove `pages/` directory when all pages are migrated

---

## Performance Gains from App Router Migration

Beyond the developer experience improvements, the App Router delivers measurable performance benefits that matter for user-facing metrics.

**Reduced JavaScript bundle size.** Server Components render on the server and send HTML — no React component code shipped to the browser. In a typical Pages Router app, every page component and its dependencies contribute to the client bundle. After migration, components that only render data (no state, no events) are zero-weight on the client. For data-heavy pages (dashboards, reports), this can reduce the initial JS load by 30–60%.

**Streaming and Suspense.** App Router pages stream HTML from the server, which means users see content earlier even for slow database queries. Wrap slow sections in `<Suspense fallback={<Skeleton />}>` and the rest of the page renders while the slow section loads. This significantly improves perceived performance, even if raw data fetching time is the same.

**Better caching control.** `fetch` in Server Components participates in Next.js's request deduplication — if two components on the same page fetch the same URL, it's one network request. And `unstable_cache` lets you control server-side data caching at a granular level, reducing database query counts per request.

**Reduced API route overhead.** Many Pages Router apps have API routes that exist solely for server-side data fetching (because you couldn't access the database directly in pages). With Server Components, these disappear — the component fetches data directly, eliminating a round-trip.

Track Core Web Vitals before and after migration. Most teams see improvement in LCP (Largest Contentful Paint) and INP (Interaction to Next Paint) after a thorough App Router migration.

## Handling the Migration in Production

For production apps with active users, the migration needs a careful approach:

**Feature flags for migrated pages.** As you migrate pages, use a feature flag to slowly roll out the new App Router version. If a migrated page has a bug, roll back the flag without a deployment.

**Canary deployments.** Deploy the App Router pages alongside the Pages Router pages. Route 5% of traffic to the App Router version, monitor error rates and performance, then increase the rollout percentage.

**Monitor the auth flows most carefully.** Auth bugs after App Router migration are the most common and most damaging failure. After migrating each auth-related page (login, signup, password reset, session handling), run manual QA on every flow before releasing.

**Keep a rollback plan.** If you remove the `pages/` directory and the migration has a critical bug, your rollback requires reverting to a previous deployment. Consider keeping `pages/` around until you've run the App Router version in production for 2–4 weeks with no issues.

---

## Related Resources

For boilerplates that already use the App Router, see [best boilerplates for AI SaaS products](/guides/best-boilerplates-ai-saas-2026) — all modern AI boilerplates ship with App Router by default. If you're customizing an existing ShipFast installation (which recently moved to App Router), see the [ShipFast customization guide](/guides/how-to-customize-shipfast-2026) for App Router-specific patterns. For adding authentication to App Router from scratch, the [Auth.js v5 vs Lucia v3 vs Better Auth comparison](/guides/authjs-v5-vs-lucia-v3-vs-better-auth-2026) covers the options.

---

## Methodology

Migration patterns based on the official Next.js migration guide and community reports from Discord and GitHub discussions. Auth.js v5 migration steps from the Auth.js v5 beta documentation. Tested against Next.js 14.2 and 15.x. The App Router migration path has stabilized significantly since early adoption — teams migrating in 2026 benefit from substantially better documentation and fewer breaking edge cases than those who migrated in 2023.

*Find modern App Router boilerplates at [StarterPick](https://starterpick.com).*
