Type something to search...
App Router vs Pages Router in Next.js: Which One Should You Use in 2026?

App Router vs Pages Router in Next.js: Which One Should You Use in 2026?

Next.js ships with two routers. The Pages Router is the original one: a pages directory where every file is a route, data loaded through getServerSideProps and getStaticProps, and a single _app.tsx wrapping everything. The App Router arrived in Next.js 13, lives in an app directory, and is built on React Server Components, nested layouts, and streaming. Both are still supported in Next.js 16, and both can live in the same project.

That leaves a real question for anyone starting a project or maintaining an older one: which router should you be using in 2026? The short answer is "the App Router for anything new", but the longer answer depends on what you already have, what your team knows, and which features you actually need.

I'll compare the two routers side by side on the things that affect your daily work: how routes are defined, how components render, how data is loaded, how layouts and loading states work, how caching behaves, and how mutations are handled. Then I'll give a concrete decision guide.

The Short Version

If you're starting a new Next.js project today, use the App Router. Every new framework feature since 2023 has landed there first (or only there): Server Components, Server Actions, streaming with Suspense, parallel and intercepting routes, the Metadata API, "use cache", and Cache Components. The official docs default to it, and most of the ecosystem's examples now assume it.

If you have a working Pages Router app, you don't have to rewrite it. The Pages Router is still maintained, still receives fixes, and still works with Next.js 16. You can adopt the App Router incrementally, one route at a time, whenever there's a reason to.

The rest of this post explains why.

How Routes Are Defined

Both routers are file-system based, but they map files to URLs differently.

In the Pages Router, a file is a route:

pages/
├── _app.tsx          # wraps every page
├── _document.tsx     # custom HTML document
├── index.tsx         # /
├── about.tsx         # /about
├── blog/
│   ├── index.tsx     # /blog
│   └── [slug].tsx    # /blog/:slug
└── api/
    └── posts.ts      # /api/posts

In the App Router, a folder is a route segment, and a special page.tsx file makes it publicly accessible:

app/
├── layout.tsx            # root layout (replaces _app and _document)
├── page.tsx              # /
├── about/
│   └── page.tsx          # /about
├── blog/
│   ├── layout.tsx        # shared UI for /blog/*
│   ├── page.tsx          # /blog
│   └── [slug]/
│       ├── page.tsx      # /blog/:slug
│       └── loading.tsx   # loading UI for this segment
└── api/
    └── posts/
        └── route.ts      # /api/posts

The folder-based approach has one practical advantage: you can colocate files. A components/ folder or a utils.ts file inside app/blog/ doesn't become a route because only page.tsx and route.ts are publicly routable. In the Pages Router, every file inside pages/ is treated as a page, so components have to live elsewhere.

The App Router also adds a set of special files per segment: layout.tsx, loading.tsx, error.tsx, not-found.tsx, template.tsx, and default.tsx. Each one handles a specific concern for that part of the URL.

Rendering: Server Components vs Client Components

This is the biggest conceptual difference.

In the Pages Router, every page component is a regular React component. It's rendered to HTML on the server (or at build time) and then hydrated in the browser. All of its code, including any libraries it imports, is shipped to the client.

In the App Router, components are Server Components by default. They run only on the server, can be async, can read from a database directly, and send no JavaScript to the browser for their own logic. You opt into client-side interactivity per component with the "use client" directive.

// app/products/page.tsx
import { AddToCartButton } from "./add-to-cart-button";
import { db } from "@/lib/db";

export default async function ProductsPage() {
  const products = await db.product.findMany();

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name}
          <AddToCartButton productId={product.id} />
        </li>
      ))}
    </ul>
  );
}
// app/products/add-to-cart-button.tsx
"use client";

import { useState } from "react";

export function AddToCartButton({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false);

  return (
    <button onClick={() => setAdded(true)} disabled={added}>
      {added ? "Added" : `Add ${productId} to cart`}
    </button>
  );
}

The page queries the database directly and ships no JavaScript for the list itself. Only the button, which needs useState and an onClick handler, becomes part of the client bundle. In the Pages Router, the equivalent page would ship the whole component tree to the browser, and the database query would have to live in getServerSideProps.

For content-heavy pages this can make a noticeable difference in bundle size. The trade-off is a new mental model: you have to think about where the server/client boundary sits and what can be passed across it (serializable props only).

Data Fetching

Pages Router

Data loading happens in page-level functions that run before the component renders:

// pages/dashboard.tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";

type Project = { id: string; name: string };

export const getServerSideProps = (async () => {
  const res = await fetch("https://api.example.com/projects");
  const projects: Project[] = await res.json();
  return { props: { projects } };
}) satisfies GetServerSideProps<{ projects: Project[] }>;

export default function Dashboard({
  projects,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <ul>
      {projects.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

This is explicit and easy to reason about. The downside is that only the page can fetch. If a sidebar component deep in the tree needs data, you either fetch it in the page and prop-drill it down, or fetch it client-side with useEffect, SWR, or TanStack Query.

App Router

Any Server Component can fetch its own data:

// app/dashboard/page.tsx
type Project = { id: string; name: string };

export default async function Dashboard() {
  const res = await fetch("https://api.example.com/projects", {
    cache: "no-store",
  });
  const projects: Project[] = await res.json();

  return (
    <ul>
      {projects.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

There's no special function name to remember. The component is async, it awaits its data, and it renders. A sidebar component can do the same thing independently, and identical fetch calls in a single render are deduplicated automatically. For non-fetch data sources, wrap the function in React's cache() to get the same deduplication.

Request data such as cookies and headers come from functions rather than a req object. In Next.js 16 these are async:

// app/settings/page.tsx
import { cookies } from "next/headers";

export default async function SettingsPage() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value ?? "light";

  return <p>Current theme: {theme}</p>;
}

The same goes for params and searchParams, which are now promises you await in pages and layouts.

Layouts

In the Pages Router, _app.tsx wraps every page. For per-section layouts, the community settled on the getLayout pattern: attach a static function to each page and call it from _app. It works, but it's a convention rather than a framework feature, and it doesn't handle nesting well.

In the App Router, layouts are built into the routing model. A layout.tsx in any folder wraps every route below it, and layouts nest automatically:

// app/dashboard/layout.tsx
import Link from "next/link";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-[220px_1fr]">
      <nav className="flex flex-col gap-2 p-4">
        <Link href="/dashboard">Overview</Link>
        <Link href="/dashboard/projects">Projects</Link>
        <Link href="/dashboard/settings">Settings</Link>
      </nav>
      <main className="p-6">{children}</main>
    </div>
  );
}

When you navigate from /dashboard/projects to /dashboard/settings, the layout doesn't re-render. Only the page inside it changes, so any client state in the sidebar (a collapsed section, a search box) is preserved. This "partial rendering" is one of the main reasons navigation feels faster in App Router apps.

Loading and Error States

The Pages Router has no built-in way to show a loading state for a page whose data is still being fetched on the server. During a client-side transition, the old page stays on screen until getServerSideProps resolves. People work around this with router events and a global progress bar.

The App Router has loading.tsx and error.tsx at the segment level:

// app/dashboard/loading.tsx
export default function Loading() {
  return <p className="animate-pulse">Loading dashboard...</p>;
}

loading.tsx wraps the page in a React Suspense boundary, so navigation is immediate and the fallback shows while the server streams the real content. You can also place Suspense boundaries around individual components to stream parts of a page independently. error.tsx works the same way for errors: it catches errors in its segment without taking down the whole app.

Caching and Static Generation

The Pages Router has a simple, page-level model:

  • getStaticProps renders at build time.
  • getStaticProps with revalidate gives you Incremental Static Regeneration.
  • getServerSideProps renders on every request.
  • getStaticPaths decides which dynamic paths to prerender.

The App Router's model is more granular and has changed more over time. In Next.js 16 there are two modes:

  • Without Cache Components (the default), routes are prerendered when possible, fetch is not cached unless you pass cache: "force-cache" or next: { revalidate }, and segment config like export const revalidate = 3600 controls ISR. generateStaticParams replaces getStaticPaths.
  • With Cache Components (cacheComponents: true in next.config.ts), everything is dynamic by default and you opt into caching with the "use cache" directive at the function or component level, paired with cacheLife and cacheTag. This is how Partial Prerendering works in Next.js 16: a static shell is served immediately and dynamic parts stream in.
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

The App Router's caching is more powerful, because you can cache a single component instead of a whole page. It's also more to learn. If you've been burned by App Router caching in Next.js 13 or 14, it's worth looking again: the defaults are much less surprising since Next.js 15, and Cache Components makes caching explicit.

Mutations: API Routes vs Server Actions

In the Pages Router, writes go through API routes in pages/api. The client calls them with fetch, and you manually refetch or update client state afterward.

The App Router supports Route Handlers (app/api/.../route.ts) for the same purpose, but it also has Server Actions: async functions marked with "use server" that you can pass directly to a form.

// app/todos/page.tsx
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

async function addTodo(formData: FormData) {
  "use server";
  const title = String(formData.get("title") ?? "");
  await db.todo.create({ data: { title } });
  revalidatePath("/todos");
}

export default async function TodosPage() {
  const todos = await db.todo.findMany();

  return (
    <>
      <form action={addTodo}>
        <input name="title" required />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>{t.title}</li>
        ))}
      </ul>
    </>
  );
}

The form works before JavaScript loads, the action runs on the server, and revalidatePath refreshes the page data in the same round trip. Treat Server Actions like public endpoints: validate input and check authorization inside every action.

What the Pages Router Still Does Well

The Pages Router isn't obsolete, and there are honest reasons to keep using it:

  • Simpler mental model. Everything is a client component, data loads in one place per page, and there's no server/client boundary to think about.
  • Built-in i18n routing. The i18n option in next.config.js handles locale prefixes and detection for you. The App Router expects you to build this with a [lang] segment and proxy.ts.
  • Library compatibility. Some older libraries assume they run in the browser and break or need wrappers inside Server Components. In the Pages Router they just work.
  • Stability of a large existing codebase. If your app works, is fast enough, and your team is productive, a rewrite has a real cost and often little user-facing benefit.
  • React version control. The Pages Router uses the React version in your package.json, while the App Router uses the React canary channel built into Next.js.

Side-by-Side Comparison

ConcernPages RouterApp Router
Route definitionFile is a routeFolder is a segment, page.tsx makes it public
Default component typeClient (hydrated)Server Component
Data fetchinggetServerSideProps, getStaticPropsasync components, fetch, any data library
Dynamic pathsgetStaticPathsgenerateStaticParams
Shared UI_app.tsx, getLayout patternNested layout.tsx files
Loading UIManual (router events)loading.tsx, Suspense streaming
Error UI_error.tsx, 404.tsxerror.tsx, not-found.tsx per segment
MutationsAPI routesServer Actions, Route Handlers
Head/SEOnext/headMetadata API (metadata, generateMetadata)
CachingPage-level SSG/ISR/SSRComponent-level with "use cache" or fetch/segment options
Advanced routingNot availableParallel routes, intercepting routes, route groups
i18n routingBuilt-in configManual ([lang] segment + proxy)

A Decision Guide

Starting a new project? Use the App Router. You get the current feature set, the default docs, and the direction the framework is going. Even a simple marketing site or blog benefits from Server Components and the Metadata API, and you won't face a migration later. If you're new to Next.js, start with what Next.js is and setting up TypeScript.

Maintaining a stable Pages Router app? Keep it. Upgrade to the latest Next.js for security and performance fixes, and only move routes to app when you have a concrete reason: a new section of the site, a page that would clearly benefit from streaming, or a feature that only exists in the App Router.

Planning a big new feature in an existing Pages Router app? Build it in app. Both directories can live in the same project. The main catch is that navigating between a Pages route and an App route is a full page load, and next/link doesn't prefetch across routers. Keeping a whole section (say, everything under /dashboard) in one router minimizes that.

Relying heavily on Pages Router i18n or a library that doesn't support Server Components? Check the migration cost carefully first. These are solvable, but they're the most common reasons a migration takes longer than planned.

Running Both Routers Together

Since app and pages can coexist, you don't need a big-bang switch. A few rules apply:

  • A URL can only be defined in one router. If both pages/about.tsx and app/about/page.tsx exist, the build fails with a conflict.
  • app/layout.tsx doesn't apply to pages/* routes, and _app.tsx doesn't apply to app/* routes. Global styles and providers need to be set up in both while you migrate.
  • Shared components should avoid next/router (Pages only) and next/navigation (App only) in the same file. next/compat/router exists for components used by both.

FAQ

Is the Pages Router deprecated? No. It's still supported and documented in Next.js 16. New features, however, are focused on the App Router.

Is the App Router slower? For most apps it's faster for users, because less JavaScript ships to the browser and layouts don't re-render on navigation. Development builds use Turbopack by default in Next.js 16 for both routers.

Can I use Server Components in the Pages Router? No. Server Components, Server Actions, and streaming with Suspense at the route level are App Router features.

Conclusion

The App Router is the default choice for new Next.js work in 2026. It gives you Server Components, nested layouts, streaming, Server Actions, and a component-level caching model that the Pages Router can't match. The Pages Router is still a reasonable place for an existing app to stay, and it remains simpler to reason about.

If you're on the fence, pick the App Router for new code and move older routes over when there's a reason to touch them. Since the two routers coexist, you can make that decision one route at a time rather than all at once.

Tags :
Share :

Related Posts

A Deep Dive into next.config Options Every Developer Should Know

A Deep Dive into next.config Options Every Developer Should Know

next.config.ts is the one file every Next.js project has and almost nobody reads end to end. It starts as an empty object, then slowly collects a r

Continue Reading
Adding JSON-LD Structured Data to Next.js Pages for Rich Search Results

Adding JSON-LD Structured Data to Next.js Pages for Rich Search Results

Search engines are good at reading pages, but they still guess. Is "4.7" a rating or a version number? Is that date when the article was published or

Continue Reading
Adding Page Transitions and Animations to Next.js with Framer Motion

Adding Page Transitions and Animations to Next.js with Framer Motion

Animation is one of the easiest ways to make an app feel polished, and one of the easiest ways to make it feel slow. A subtle fade when a page loads,

Continue Reading