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.
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 |
# 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
// 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/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.
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.
// 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.
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/ and app/ 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.
For any new project started in 2026: use App Router. The reasons:
If you're on Pages Router and want to migrate incrementally:
pages/ intact — both routers can coexistapp/layout.tsx firstapp/api/ as you touch thempages/ 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
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.
Request/Response — more portable than Pages Router API routes