Next.js App Router vs Pages Router: Which Should You Use?
A practical comparison of Next.js App Router and Pages Router — when to migrate, what actually changed, and which one makes sense for your project in 2026.
The App Router was introduced in Next.js 13 and became stable in 14. In 2026, it's the default for new projects. But understanding what actually changed — and when the Pages Router is still acceptable — helps you make better architectural decisions.
What Actually Changed
The App Router isn't just a new file structure. It's a fundamentally different rendering model based on React Server Components (RSC). The Pages Router renders everything as client-side React with optional server-side data fetching functions. The App Router treats server rendering as the default and client rendering as opt-in.
| Pages Router | App Router | |
|---|---|---|
| Default component type | Client Component | Server Component |
| Data fetching | getServerSideProps, getStaticProps | async component functions |
| Layouts | Manual _app.tsx | Nested layout.tsx files |
| Loading states | Manual | loading.tsx convention |
| Error handling | _error.tsx | error.tsx per segment |
| Streaming | Not supported | Built-in with Suspense |
| API routes | pages/api/* | app/api/*/route.ts |
File Structure Comparison
# Pages Router
pages/
├── _app.tsx # Global layout
├── _document.tsx # HTML document
├── index.tsx # / route
├── about.tsx # /about route
├── blog/
│ ├── index.tsx # /blog route
│ └── [slug].tsx # /blog/:slug route
└── api/
└── posts.ts # /api/posts endpoint
# App Router
app/
├── layout.tsx # Root layout (replaces _app + _document)
├── page.tsx # / route
├── about/
│ └── page.tsx # /about route
├── blog/
│ ├── layout.tsx # Shared blog layout
│ ├── page.tsx # /blog route
│ ├── loading.tsx # Loading UI
│ └── [slug]/
│ └── page.tsx # /blog/:slug route
└── api/
└── posts/
└── route.ts # /api/posts endpoint
Data Fetching: The Biggest Difference
Pages Router
// pages/blog/[slug].tsx
export async function getServerSideProps({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) return { notFound: true }
return { props: { post } }
}
export default function BlogPost({ post }) {
// post is always available — no loading state needed
return <article>{post.title}</article>
}// Static generation with Pages Router
export async function getStaticProps({ params }) {
const post = await getPostBySlug(params.slug)
return { props: { post }, revalidate: 3600 }
}
export async function getStaticPaths() {
const slugs = await getAllPostSlugs()
return { paths: slugs.map(slug => ({ params: { slug } })), fallback: 'blocking' }
}App Router
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
return getAllPostSlugs().map(slug => ({ slug }))
}
// The component IS the data fetching — no separate function needed
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug)
if (!post) notFound()
return <article>{post.title}</article>
}The App Router eliminates the mental overhead of matching data fetching functions to components. The component fetches its own data and renders it — simpler to read and reason about.
Nested Layouts
This is where App Router has a genuine advantage for complex UIs:
// app/dashboard/layout.tsx — wraps all /dashboard/* routes
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
)
}
// app/dashboard/settings/layout.tsx — nested inside dashboard layout
export default function SettingsLayout({ children }) {
return (
<div>
<SettingsTabs />
{children}
</div>
)
}In Pages Router, you'd implement this manually in each page or through complex _app.tsx logic. With App Router, the file system defines the layout hierarchy.
Route Handlers vs API Routes
// Pages Router: pages/api/posts.ts
export default function handler(req, res) {
if (req.method === 'GET') {
res.json({ posts: [] })
}
}
// App Router: app/api/posts/route.ts
export async function GET(request: Request) {
return Response.json({ posts: [] })
}
export async function POST(request: Request) {
const body = await request.json()
// ...
return Response.json({ success: true }, { status: 201 })
}Route Handlers use the web standard Request/Response APIs instead of Node.js req/res. They can also run on the Edge Runtime.
When to Use Pages Router
Despite App Router being the default, Pages Router is still appropriate when:
-
You have a large existing codebase — migrating a 200-page Pages Router app to App Router is a significant project. Run both concurrently instead (
pages/andapp/can coexist). -
Your team is new to React Server Components — the RSC model requires a shift in mental model. For teams under tight deadlines, Pages Router might be safer short-term.
-
Heavy third-party dependencies — some libraries still assume a browser DOM and don't work in Server Components. Context-heavy UI libraries are a common pain point.
When to Use App Router
For any new project started in 2026: use App Router. The reasons:
- Server Components reduce JavaScript bundle size significantly
- Built-in streaming prevents slow data from blocking the whole page
- Nested layouts make complex UI hierarchies maintainable
- The direction Next.js is investing in — Pages Router gets bug fixes, not new features
Migration Strategy
If you're on Pages Router and want to migrate incrementally:
- Keep
pages/intact — both routers can coexist - Move layout logic to
app/layout.tsxfirst - Migrate one route at a time, starting with simple pages
- Move API routes to
app/api/as you touch them - Remove
pages/once all routes are migrated
# Both directories work simultaneously
pages/
legacy-page.tsx # Still works
app/
new-page/
page.tsx # New page works alongside legacy
Common Mistakes When Migrating
1. Adding 'use client' everywhere. This defeats the purpose of App Router. Only use it when you need hooks or browser APIs.
2. Forgetting async on data-fetching components. Server Components can be async — use it.
3. Nesting Client Components around Server Components. Client Components can't import Server Components. Pass them as children instead.
4. Expecting getServerSideProps patterns to work. App Router has no getServerSideProps. Data fetching happens in the component itself.
Key Takeaways
- App Router is the right choice for all new Next.js projects in 2026
- The key change is the rendering model: Server Components by default, Client Components opt-in
- Nested layouts in App Router are cleaner and more maintainable than Pages Router approaches
- Pages Router and App Router can coexist — migrate incrementally rather than all at once
- Route Handlers use web standard
Request/Response— more portable than Pages Router API routes

Written by
Sabir KhaloufiFull-stack developer and tech blogger sharing in-depth tutorials on React, Next.js, AI, and modern web development.