Next.js App Router vs Pages Router: What's the Difference
The App Router uses React Server Components and nested layouts; the Pages Router is Next.js's original file-based model. How they differ and when to pick each.
Next.js ships two different ways to structure a project: the App Router, built around React Server Components and nested layouts, and the Pages Router, the original file-based routing model the framework launched with. Both are still supported, but they lead to noticeably different code, so picking the right one up front saves a painful migration later.
How the two routers are structured
The Pages Router maps files in a pages/ directory directly to routes: pages/about.js becomes /about, and pages/posts/[id].js becomes /posts/:id. Data fetching happens through exported functions like getServerSideProps or getStaticProps, and every component in the tree is a client component by default — it ships JavaScript to the browser and can use hooks like useState freely.
The App Router uses an app/ directory where folders map to route segments and a page.js file inside a folder marks it as a routable page. Layouts are defined with layout.js files that wrap their child segments and persist across navigations without re-rendering. Components inside app/ are React Server Components by default — they render on the server, send no JavaScript to the client, and can await data directly inside the component body instead of exporting a separate data-fetching function. Reaching for useState, useEffect, or browser APIs requires an explicit "use client" directive at the top of the file. For a deeper look at what that default actually buys you, see what React Server Components are.
Comparison
| Pages Router | App Router | |
|---|---|---|
| Directory | pages/ | app/ |
| Rendering default | Client component | Server component |
| Data fetching | getServerSideProps / getStaticProps | async/await inside components |
| Layouts | Manual, via _app.js and custom wrappers | Native, via nested layout.js files |
| Loading and error states | Manual conditional rendering | Convention-based loading.js / error.js |
| Streaming | Limited | Built in, via Suspense boundaries |
| Maturity | Stable since Next.js’s early releases | Newer, actively the primary investment |
Why Server Components change the data-fetching story
In the Pages Router, a page component and its data-fetching function are two separate exports that Next.js stitches together at build or request time. In the App Router, a server component can just await fetch() (or a database call) directly in its body, and the result renders into HTML on the server — no client-side loading state needed for that initial render. This cuts down on the amount of client-side JavaScript and lets you compose data-fetching close to where it’s used, similar in spirit to how Astro’s islands architecture keeps most of a page static and only hydrates the interactive parts.
Streaming is the other big shift. Because server components can be rendered independently and wrapped in <Suspense>, a slow data source doesn’t have to block the whole page — the shell can ship immediately while individual sections stream in as their data resolves. If you’re new to Suspense boundaries in this context, React Suspense covers the mechanics in more depth.
Client components still matter
Not everything belongs on the server. Anything that needs interactivity — form state, click handlers, browser-only APIs, effects — still needs to be a client component, marked with "use client". The App Router’s model is to push client components as far down the tree as possible: a page can be mostly server-rendered, with a small interactive widget (a like button, a dropdown) as the only client component embedded in it. State libraries you’d reach for inside those client components, like Zustand or Redux, work the same way they always have — they just can’t run on the server.
Migration considerations
Next.js allows both routers to coexist in the same project during a migration — routes not yet moved to app/ keep working from pages/. That said, a few things trip people up:
- Data libraries built around client-side fetching (some React Query patterns, for example) need adjustment since server components can fetch directly and don’t need a client-side cache for the initial load.
- Context providers (theme, auth) generally need to live in a client component boundary, since React context isn’t available inside server components.
- Third-party libraries that assume they’re always running in the browser (accessing
windowat module scope, for instance) will error inside a server component unless they’re wrapped in a client boundary.
When to choose which
For a new project, the App Router is the more capable option — smaller client bundles, native layouts and streaming, and the direction Next.js is actively building toward. The Pages Router remains a reasonable choice for a small, mostly-static site that doesn’t need any of that, or for teams with a large existing Pages Router codebase where a full rewrite isn’t worth the cost right now. There’s no requirement to migrate an already-working Pages Router app just to be on the newer model.
The takeaway
The Pages Router and App Router are two different rendering models bolted onto the same framework: file-to-route mapping and client-first rendering versus nested layouts, server components by default, and built-in streaming. New projects should start with the App Router; existing Pages Router apps can keep running as-is or migrate incrementally, since Next.js supports both directories side by side.
Keep reading
Takina · · 5 min read React Server Components, Explained Without the Hype
React Server Components render exclusively on the server and stream a serialized result — no client JS shipped for that component. Here's what that actually means.
Takina · · 5 min read TanStack Query vs SWR: Choosing a Data-Fetching Library
TanStack Query and SWR both cache and revalidate server data in React, but differ in scope, mutation handling, and how much they manage for you.
Takina · · 4 min read React Compiler: Automatic Memoization Explained
The React Compiler automatically memoizes components and values at build time, cutting most manual useMemo, useCallback, and React.memo calls.