from the Pages Router: the complete guide
The App Router is not just a new directory, it fundamentally changes the mental model of Next.js. Layouts, Server Components, nested routes and the new data fetching paradigm require a thought-out migration plan that avoids production outages and does not overwhelm the team.
Table of Contents
- 1. Why the App Router is more than a refactor
- 2. New directory structure and routing conventions
- 3. Understanding and using React Server Components
- 4. Layouts: shared UI without re-render
- 5. Data fetching: from getServerSideProps to async components
- 6. Route Handlers replace API Routes
- 7. Client Components: using use client deliberately
- 8. Incremental migration strategy
- 9. Pages Router vs. App Router in direct comparison
- 10. Summary
- 11. FAQ
1. Why the App Router is more than a refactor
The Next.js App Router has been stable since version 13 and is the recommended default architecture since version 14. Anyone who treats the migration from the Pages Router as simply moving files will quickly run into resistance: the underlying mental model is fundamentally different. Instead of the classic request-response cycle with getServerSideProps or getStaticProps, the App Router uses React Server Components as its first abstraction, which are rendered server-side by default and load no client bundle.
The result is significantly improved Core Web Vitals performance: since Server Components send no JavaScript to the browser, the initial bundle shrinks considerably. At the same time, the new layouts architecture allows only the changing segments of a page to be re-rendered, navigation, sidebars and headers stay in memory and are not remounted on every route change. The App Router migration pays off for almost every existing Next.js project, but it requires a structured plan.
The most important step before migrating: understand which parts of the application need genuine interactivity (browser events, useState, useEffect) and which are purely presentational. Presentational components become Server Components and gain direct access to databases, the file system and secrets, without a separate API endpoint being necessary. Interactive parts are marked with the 'use client' directive and remain Client Components.
2. New directory structure and routing conventions
In the App Router, the app/ directory replaces the former pages/ directory. Both can coexist during migration, Next.js recognizes them and treats them as separate routers. Routes are defined by folders, not by files. A page needs a page.tsx file in the corresponding folder, a layout needs a layout.tsx. Special files such as loading.tsx, error.tsx, not-found.tsx and template.tsx apply automatically to the respective route segment.
Dynamic segments still work with square brackets ([slug]), and catch-all segments ([...slug]) as well as optional catch-all segments ([[...slug]]) behave identically to the Pages Router. New are route groups with parentheses: (marketing)/about/page.tsx and (shop)/about/page.tsx both produce the route /about, but have separate layouts and middleware scopes. That is particularly useful when different areas of an application need completely different shell layouts.
// app/layout.tsx: root layout replaces pages/_app.tsx and pages/_document.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { template: '%s | Mironsoft', default: 'Mironsoft' },
description: 'Next.js App Router example',
}
// Root layout MUST return html and body elements
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="de">
<body className="bg-white text-gray-900 antialiased">
<header className="sticky top-0 z-50 border-b bg-white/80 backdrop-blur">
<nav className="mx-auto max-w-7xl px-4 py-4">Navigation</nav>
</header>
<main>{children}</main>
<footer className="border-t py-8">Footer</footer>
</body>
</html>
)
}
// app/(shop)/layout.tsx: nested layout for the shop segment
export default function ShopLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex gap-8">
<aside className="w-64 shrink-0">Sidebar</aside>
<div className="flex-1">{children}</div>
</div>
)
}
3. Understanding and using React Server Components
In the App Router, all components are Server Components by default. That means: they run exclusively on the server, have access to Node.js APIs, can communicate directly with the database and send no JavaScript to the browser. That is a fundamental shift compared to the Pages Router, where all components ran client-side and could only receive data through special props functions (getServerSideProps, getStaticProps).
Server Components can be async and await database queries or external APIs directly, without useEffect or useState. That eliminates the common rendering cycle: component renders empty, data loads, component renders again with data. Instead, the server waits for all data and sends fully rendered HTML to the browser. That significantly improves perceived performance, especially on slow connections. Fetch requests in Server Components are automatically deduplicated and cached by Next.js.
The most important restriction: Server Components must not use browser APIs, must not hold state and must not register event handlers. If a component needs onClick, useState or useEffect, it must be a Client Component. The composition pattern is crucial here: a Server Component can import and render a Client Component as a child, but not the other way around. Data is passed as props from the Server Component to the Client Component.
4. Layouts: shared UI without re-render
The layouts system of the App Router solves one of the most persistent problems in the Pages Router: shared UI elements such as navigation, sidebar or footer were remounted and re-rendered on every page change, because Next.js swapped out the entire page. In the App Router, layouts remain stable in the DOM while navigating within their segment, only the page.tsx component underneath is replaced. That means: no lost scroll positions in the sidebar, no re-triggered animations in the navigation, no flickering header.
Layouts can be nested arbitrarily deep. The root layout wraps the entire application, a shop layout wraps all shop pages, a product layout wraps all product pages. Each layout receives its segment-specific children prop and can load its own data, without that data having to be passed down to all child pages as props. That results in a cleaner component hierarchy, where every layer only knows the data it actually needs.