When Next.js introduced the App Router in version 13, it was not a minor update. It was a rethinking of what a React framework should be — one built not around workarounds and special export functions, but around what React had become capable of at the server level. Understanding the App Router means understanding the philosophy behind it, not just its mechanics.
What the App Router Is
The App Router is Next.js's routing architecture, introduced in version 13 and now the recommended default. It lives inside the app/ directory and governs how routes are defined, how pages are rendered, how layouts are composed, and how data moves from server to client. Everything about a Next.js application — its pages, its API endpoints, its loading states, its error handling — is expressed through the conventions of the App Router.
What makes the App Router distinct from its predecessor is what it is built on: React Server Components. This is not a detail — it is the foundation. The App Router is, at its core, a system for composing Server Components into applications, with routing, layouts, and data fetching as the scaffolding around them.
The Problem with the Old Approach
To appreciate why the App Router exists, it helps to understand what it replaced.
The previous system — the Pages Router — treated every file in a pages/ directory as a React component that ran in the browser. Data loading required special exported functions — getServerSideProps for data fetched on every request, getStaticProps for data fetched at build time. These functions ran on the server, but the components themselves were always client-side constructs. The result was an artificial separation: data logic sat in an exported function, UI logic sat in the component, and the two communicated through props injected by the framework at render time.
This worked. Millions of applications were built on it. But it had limits. The components always shipped JavaScript to the browser. The data-fetching pattern was rigid — a page fetched its data as a unit rather than letting individual components fetch what they needed. And the architecture did not align with the direction React itself was moving.
React had been developing Server Components — a model where components run exclusively on the server, produce HTML directly, and send no JavaScript to the client whatsoever. The Pages Router had no way to incorporate this. The App Router was built from the ground up to embrace it.
Server Components as the Default
The single most important thing to understand about the App Router is this: every component is a Server Component by default.
A Server Component runs on the server. It has access to server-side resources — databases, file systems, environment variables, internal APIs. It produces HTML output that is sent to the client. It ships no JavaScript to the browser. From the browser's perspective, the output of a Server Component is just HTML — there is no React reconciliation, no event listeners, no hydration overhead for that component.
This is a significant shift. In the Pages Router era, components existed in the browser. In the App Router, components exist on the server unless explicitly moved to the client. The browser gets HTML plus only the JavaScript needed for the interactive parts of the page.
The practical consequences are substantial. A page that displays blog posts can fetch those posts directly in the component without an API call from the browser, without useEffect, without loading state management — because the data is fetched on the server before the HTML is sent. The client receives a fully populated page.
// app/blog/page.tsx
// This is a Server Component — no 'use client' directive
// It runs on the server and can fetch data directly
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
No useEffect. No loading state. No API call from the browser. The data is fetched during rendering on the server, the HTML is built, and the client receives a complete page immediately.
Client Components
Not all components can or should run on the server. Anything that requires interactivity — event handlers, browser APIs, React state, React hooks — must run in the browser. These are Client Components, and they are opted into explicitly with the 'use client' directive at the top of the file.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
The 'use client' directive does not mean the component only runs in the browser — it means the component runs in both environments. It is rendered on the server for the initial HTML, then hydrated in the browser to become interactive. What it cannot do is use server-only resources: it cannot access a database directly, cannot read environment variables without the NEXT_PUBLIC_ prefix, and cannot use Node.js APIs.
The distinction between Server and Client Components is covered in its own dedicated topic later in this series. What matters here is understanding that the App Router is built around this distinction — it is not an implementation detail, it is the architecture.
Nested Routing and Layouts
The App Router introduced a more powerful model for layouts than the Pages Router offered. In the Pages Router, layouts were a convention developers implemented themselves — typically a _app.tsx file or a wrapping component. The App Router makes layouts a first-class primitive.
A layout.tsx file at any level of the app/ directory wraps all routes beneath it. Layouts persist across navigation — they do not unmount and remount when the user moves between routes within their scope. This is what makes a consistent navigation bar, sidebar, or shell possible: the layout renders once and stays, while the page content inside it changes.
app/
├── layout.tsx ← wraps every page in the application
├── page.tsx → /
└── dashboard/
├── layout.tsx ← wraps every page under /dashboard
├── page.tsx → /dashboard
└── settings/
└── page.tsx → /dashboard/settings
When a user navigates from /dashboard to /dashboard/settings, the root layout and the dashboard layout both remain mounted. Only the page component itself changes. This is more efficient than remounting the entire UI on every navigation, and it is the correct model for applications with persistent chrome.
Streaming and Suspense
The App Router is designed around React's Suspense model for asynchronous rendering. Rather than waiting for all data to be ready before sending any HTML to the browser, the App Router can stream the response — sending the parts of the page that are ready immediately and filling in the rest as data becomes available.
This is where the loading.tsx file convention becomes meaningful. Placing a loading.tsx file inside a route directory automatically wraps that route's page in a React Suspense boundary. While the page's data is being fetched on the server, the loading component is shown to the user instead of a blank space.
app/
└── dashboard/
├── loading.tsx ← shown while dashboard/page.tsx awaits data
└── page.tsx → /dashboard
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return <div>Loading dashboard...</div>;
}
The user sees the loading state immediately — the shell of the page arrives before the data does. When the server finishes fetching the data and rendering the page component, it streams the result to the browser, which replaces the loading state automatically.
This is a fundamentally better user experience than waiting for all data before showing anything, and it requires no manual state management. The file convention handles it.
Error Handling
Alongside loading.tsx, the App Router provides error.tsx — a file that defines what users see when something goes wrong within a route segment. It functions as a React error boundary, catching errors thrown by the page or any of its children and rendering the error component instead of crashing the entire application.
// app/dashboard/error.tsx
'use client'; // Error components must be Client Components
export default function DashboardError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong in the dashboard.</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Error components must be Client Components — they need to capture the error on the client and provide recovery mechanisms like the reset function, which retries rendering the failed segment. Placing error.tsx at the route level means errors are contained to that segment rather than propagating upward and taking down the entire page.
How the App Router Handles a Request
Understanding the sequence of events when a user makes a request clarifies how the App Router's pieces connect.
When a request arrives, Next.js matches the URL to the appropriate route in the app/ directory. It identifies the full hierarchy of layouts that apply to that route — from the root layout down through any nested layouts — and the page component at the centre.
Next.js renders this tree on the server. Server Components run and produce their output. Any data fetching they perform happens during this render, not after. The result is a stream of HTML that begins flowing to the browser: the outer layouts first, then the page content, with Suspense boundaries serving as placeholders for any sections whose data is still being awaited.
The browser receives the HTML and displays it immediately. For the parts of the page that are Client Components, React hydrates them — attaching event listeners and activating interactivity — without re-rendering the HTML that already exists.
For subsequent navigations within the same application, the App Router does not perform full page reloads. It fetches only what changed — the new page segment — while preserving the layouts that did not change. The result is navigation that feels as fast and seamless as a single-page application while retaining the server-rendering benefits of a traditional web architecture.
The App Router vs the Pages Router
For anyone who has worked with Next.js before the App Router, the comparison is worth making explicit.
| Pages Router | App Router | |
|---|---|---|
| Default component type | Client Component | Server Component |
| Data fetching | getServerSideProps / getStaticProps | Directly in components, using async/await |
| Layouts | Manual convention (_app.tsx) | Built-in, file-based (layout.tsx) |
| Loading states | Manual state management | Built-in (loading.tsx) |
| Error handling | Manual error boundaries | Built-in (error.tsx) |
| Streaming | Not supported | Supported natively via Suspense |
| Routing directory | pages/ | app/ |
The App Router does not simply add features to the Pages Router model — it replaces the underlying model entirely. Components are server-first. Data fetching is co-located with the components that need it. Layouts, loading, and error states are expressed through file conventions rather than code. The framework does more; the developer writes less.
Why This Matters
The App Router represents a meaningful shift in what a web application can be at the architecture level. Pages that fetch their own data on the server before rendering, without a round-trip to an API from the browser. Layouts that persist across navigation. Loading states that appear before data is ready, without manual implementation. Error boundaries scoped to individual route segments.
Each of these capabilities existed before the App Router, but they required significant boilerplate, third-party libraries, or careful manual implementation. The App Router makes them the default — the thing that happens unless a developer actively chooses otherwise.
The topics that follow in this series build on the App Router as a foundation. Routing, layouts, Server Components, data fetching — each is a specific dimension of the architecture introduced here. Understanding the App Router as a whole, before examining any of its parts in isolation, is what makes those parts cohere into something legible rather than a collection of independent features.