Next.js App Router vs Pages Router: Which to Use in 2026?

Next.js 13 introduced the App Router alongside the existing Pages Router. Three years later, the App Router is the default and the recommended path for new projects. But the Pages Router still works, still receives maintenance updates, and is often the right choice in specific situations. This guide explains the real differences so you can make an informed decision.

The Core Difference: Server Components

The fundamental shift in the App Router is React Server Components (RSC). In the Pages Router, all components are client components — they ship JavaScript to the browser and can run on the client. In the App Router, components are server components by default — they render on the server, ship no JavaScript to the browser, and can directly access databases, file systems, and environment variables.

// App Router — app/posts/page.tsx
// This is a Server Component by default — no "use client", no useEffect, no loading state
import { prisma } from '@/lib/prisma'

export default async function PostsPage() {
    // Fetch directly in the component — runs on the server, never in the browser
    const posts = await prisma.post.findMany({
        where:   { published: true },
        orderBy: { createdAt: 'desc' },
    })

    return (
        <ul>
            {posts.map(post => (
                <li key={post.id}>{post.title}</li>
            ))}
        </ul>
    )
}
// Pages Router equivalent — pages/posts.tsx
// Must use getServerSideProps to fetch on the server
import { prisma } from '@/lib/prisma'
import type { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async () => {
    const posts = await prisma.post.findMany({ where: { published: true } })
    return { props: { posts } }
}

export default function PostsPage({ posts }) {
    return (
        <ul>
            {posts.map(post => (
                <li key={post.id}>{post.title}</li>
            ))}
        </ul>
    )
}

File Structure Comparison

Pages Router Structure

pages/
├── index.tsx             # → /
├── about.tsx             # → /about
├── blog/
│   ├── index.tsx         # → /blog
│   └── [slug].tsx        # → /blog/my-post
├── api/
│   └── users.ts          # → /api/users (API route)
└── _app.tsx              # global layout wrapper

App Router Structure

app/
├── page.tsx              # → /
├── layout.tsx            # root layout (replaces _app.tsx)
├── about/
│   └── page.tsx          # → /about
├── blog/
│   ├── page.tsx          # → /blog
│   └── [slug]/
│       └── page.tsx      # → /blog/my-post
└── api/
    └── users/
        └── route.ts      # → /api/users (Route Handler)

Data Fetching Patterns

Pages Router: Three Functions

// getStaticProps — build time (SSG)
export const getStaticProps: GetStaticProps = async () => {
    const data = await fetch('...').then(r => r.json())
    return { props: { data }, revalidate: 60 }  // ISR: regenerate every 60s
}

// getStaticPaths — required for dynamic SSG routes
export const getStaticPaths: GetStaticPaths = async () => {
    const posts = await prisma.post.findMany({ select: { slug: true } })
    return {
        paths: posts.map(p => ({ params: { slug: p.slug } })),
        fallback: 'blocking',
    }
}

// getServerSideProps — request time (SSR)
export const getServerSideProps: GetServerSideProps = async (ctx) => {
    const session = await getSession(ctx)
    if (!session) return { redirect: { destination: '/login', permanent: false } }
    return { props: { user: session.user } }
}

App Router: Fetch + Cache Directives

// SSG — static by default, cached forever
async function StaticPage() {
    const data = await fetch('https://api.example.com/data').then(r => r.json())
    return <div>{data.title}</div>
}

// ISR — revalidate every 60 seconds
async function ISRPage() {
    const data = await fetch('https://api.example.com/data', {
        next: { revalidate: 60 }
    }).then(r => r.json())
    return <div>{data.title}</div>
}

// SSR — no cache (fresh on every request)
async function SSRPage() {
    const data = await fetch('https://api.example.com/data', {
        cache: 'no-store'
    }).then(r => r.json())
    return <div>{data.title}</div>
}

// Route segment config (alternative syntax)
export const revalidate = 60         // ISR for entire route
export const dynamic    = 'force-dynamic'  // SSR for entire route

Layouts

Pages Router: _app.tsx

// pages/_app.tsx — wraps every page
export default function App({ Component, pageProps }) {
    return (
        <AuthProvider>
            <Header />
            <Component {...pageProps} />
            <Footer />
        </AuthProvider>
    )
}

App Router: Nested Layouts

// app/layout.tsx — root layout
export default function RootLayout({ children }) {
    return (
        <html lang="en">
            <body>
                <Header />
                {children}
                <Footer />
            </body>
        </html>
    )
}

// app/dashboard/layout.tsx — nested layout for /dashboard/*
// Persists between dashboard page navigations without re-rendering
export default function DashboardLayout({ children }) {
    return (
        <div className="dashboard">
            <Sidebar />
            <main>{children}</main>
        </div>
    )
}

Nested layouts are one of App Router's biggest practical advantages — a sidebar that doesn't unmount when you navigate between dashboard pages, shared state preserved across subroutes, and per-section loading/error states.

Client Components in App Router

When you need interactivity (useState, useEffect, event handlers), add 'use client' at the top of the file:

'use client'

import { useState } from 'react'

export default function SearchBar() {
    const [query, setQuery] = useState('')
    return (
        <input
            value={query}
            onChange={e => setQuery(e.target.value)}
            placeholder="Search..."
        />
    )
}

The pattern is to keep Server Components as the outer shell (data fetching, layout) and push interactivity into small Client Components at the leaves of the tree. This maximizes the amount of code that stays server-side.

API Routes vs Route Handlers

// Pages Router: pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    if (req.method === 'GET') {
        const users = await prisma.user.findMany()
        res.json(users)
    } else if (req.method === 'POST') {
        const user = await prisma.user.create({ data: req.body })
        res.status(201).json(user)
    }
}
// App Router: app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET() {
    const users = await prisma.user.findMany()
    return NextResponse.json(users)
}

export async function POST(req: NextRequest) {
    const body = await req.json()
    const user = await prisma.user.create({ data: body })
    return NextResponse.json(user, { status: 201 })
}

Loading, Error, and Not Found States

App Router has built-in file conventions for these:

app/blog/
├── page.tsx          # the page
├── loading.tsx       # shown while page.tsx is streaming (Suspense boundary)
├── error.tsx         # shown if page.tsx throws ('use client' required)
└── not-found.tsx     # shown when notFound() is called in page.tsx
// app/blog/loading.tsx
export default function Loading() {
    return <div className="skeleton">Loading posts...</div>
}

// app/blog/error.tsx
'use client'
export default function Error({ error, reset }) {
    return (
        <div>
            <h2>Something went wrong</h2>
            <button onClick={reset}>Retry</button>
        </div>
    )
}

When to Use Each

Choose App Router for new projects if:

  • Starting a new Next.js project (always — it's the default and the future)
  • You want Server Components to reduce client-side JavaScript
  • You need nested persistent layouts (dashboard with sidebar, multi-step forms)
  • You're building with React 18+ features (Suspense streaming, concurrent rendering)
  • TypeScript is your stack (App Router's type inference is better)

Stick with Pages Router if:

  • Existing large codebase that would require significant migration effort
  • Heavy reliance on third-party libraries not yet compatible with Server Components (some auth libraries, older state management)
  • Your team isn't yet comfortable with the Server Component mental model
  • Simple sites where the App Router's complexity isn't justified

Summary

App Router is the right choice for all new Next.js projects in 2026. Server Components reduce JavaScript bundle size, nested layouts solve a real pain that Pages Router couldn't handle elegantly, and file-based loading/error/not-found states clean up a lot of boilerplate. The Pages Router isn't going away — Vercel committed to maintaining it — but the ecosystem, documentation, and innovation is all moving toward App Router.

Frequently Asked Questions

Can I use both App Router and Pages Router in the same Next.js project?

Yes — Next.js supports mixing both routers. Routes in app/ use the App Router; routes in pages/ use the Pages Router. This coexistence is intentional to support incremental migration. You might keep pages/_app.tsx for legacy pages while building new features in app/. The one constraint: the same route cannot exist in both pages/ and app/ simultaneously — the App Router takes priority if both exist.

Do Server Components work with Next.js API Routes?

Server Components don't call API routes — they call server-side code directly. You can import a Prisma query or a service function directly into a Server Component without going through an HTTP request. API routes (Route Handlers in App Router) exist for external callers: your mobile app, third-party integrations, or client components that need to make fetch requests. This eliminates a lot of API boilerplate for internal data access.

How does authentication work in App Router?

The most common approach in 2026 is NextAuth.js v5 (now called Auth.js), which has first-class App Router support. Auth.js provides a auth() function usable in Server Components, Route Handlers, and middleware. For manual JWT auth, use middleware (middleware.ts at the project root) to check tokens and redirect unauthenticated requests before they reach the page. Pass the session to Server Components via headers() or cookies() from next/headers.

Is App Router production-ready for large applications?

Yes. As of Next.js 14+, the App Router is stable and used in production by large applications including parts of Vercel's own platform. The ecosystem has largely caught up — major libraries including NextAuth, React Query (with experimental server-side support), and most UI libraries support Server Components or provide clear guidance for App Router usage. The early instability issues (excessive client-side re-renders, confusing caching) were largely resolved in Next.js 15 with a more predictable caching model (opt-in caching rather than opt-out).