4 min read
Deep Dive: How the Next.js App Router Actually Works
A comprehensive guide to the architecture and mental model behind the Next.js App Router
- frontend
- nextjs
- architecture

The Paradigm Shift
Moving from the Pages directory to the App Router in Next.js feels like learning React all over again for the first time.
Just create a page.tsx and you are good to go, right?
If you are coming from the standard React SPA world, the flow usually looks like this:
Client requests page -> Server sends empty HTML + huge JS bundle -> Browser executes JS -> API calls happen -> UI appears.
In the App Router, No.
It's actually inverted. What you are doing is receiving fully formed HTML from the server first.
So what is the fundamental difference?
The difference is -
- Components are Server-first by default
- Layouts are nested and persistent
- Navigation behaves differently (Soft vs Hard)
- The Network tab looks completely different (The RSC Payload)
The Foundation
Server Components by Default
Let's start with the biggest change.
I've built a simple dashboard where users see their profile and a list of settings.
In the old world (Pages router or standard React), this would be a Client Component.
Here's what the mental model usually looks like:
// This is how we used to think
import { useEffect, useState } from 'react';
export default function Dashboard() {
// Client side logic immediately
const [data, setData] = useState(null);
useEffect(() => {
// Fetching happens on the browser
}, []);
return <div>...</div>;
}In the App Router, this page.tsx is never sent to the browser as JavaScript.
It runs entirely on the server.
// app/dashboard/page.tsx
import db from '@/lib/db';
// This component runs ONLY on the server
export default async function Dashboard() {
const data = await db.user.findFirst();
// Only the resulting HTML and JSON data is sent to the client
return <div>Hello {data.name}</div>;
}This approach:
- Reduces bundle size (dependencies used here don't go to the client)
- Direct Database access (no API route needed for GETs)
- Improved First Contentful Paint (FCP)
The Client Boundary
A common mistake is thinking 'use client' makes a component "Client Side Only".
It actually marks a boundary.
When you add 'use client' to a file, you are telling Next.js:
"From this point down the tree, allow React hooks and event listeners."
// components/interactive-button.tsx
'use client';
import { useState } from 'react';
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
// components/interactive-button.tsx
export default function LikeButton() {
// We can use state here because of the directive above
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes((l) => l + 1)}>{likes}</button>;
}Crucially: A Client Component is still pre-rendered on the server to generate the initial HTML. It is then "hydrated" on the client.
The Architecture
Nested Layouts & Persistence
The App Router introduces a file-system based router that focuses on nested layouts.
If I navigate from /settings/profile to /settings/account, the root layout and the settings layout do not re-render. They persist. Only the page segment changes.
Here is the structure:
app/
├── layout.tsx # Root Layout (html/body tags)
├── page.tsx # Home Page
└── settings/
├── layout.tsx # Settings Sidebar (Persists on nav)
├── page.tsx # /settings
└── profile/
└── page.tsx # /settings/profile
This persistence is huge for performance because React doesn't need to diff or rebuild the sidebar or header when you switch sub-pages.
The Router Cache and Navigation
This is where developers often get confused. "Why isn't my data updating when I click a link?"
Next.js uses an aggressive Client-side Router Cache.
The Soft Navigation
When you click a <Link href="/about" />:
- Next.js does not fetch a full HTML document.
- It fetches the RSC Payload (a special JSON-like format).
- It merges this payload into the current DOM.
This is called a Soft Navigation. The browser state is preserved.
If you open your Network tab and filter by Fetch/XHR while navigating, you won't see HTML. You will see a text stream that looks like this:
1:I["module-id",["css-id"],""]
2:"$Sreact.suspense"
...
This is the server telling the client tree how to update without a full reload.
Solution: Managing the Cache
If you need data to refresh immediately upon navigation (for example, after a mutation), you cannot rely on standard Link behavior alone if the data is dynamic.
You have two main levers:
- Route Segment Config
Force the page to be dynamic on the server.
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // Opt out of static caching
- On-demand Revalidation
When performing an action, tell Next.js to purge the cache.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function updateUser() {
await db.user.update(...);
// This tells the Router Cache: "The data for this path is stale"
revalidatePath('/dashboard');
}Special Files
The App Router relies on specific file conventions to handle UI states automatically. You don't need to manage loading states manually in your components.
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="skeleton-loader">Loading Dashboard...</div>;
}Next.js automatically wraps your page.tsx in a <Suspense> boundary using this component as the fallback.
Similarly:
error.tsx: Wraps in an Error Boundarynot-found.tsx: Handles 404slayout.tsx: Wraps the children
Summary
- Think Server-First: Components are server-side by default; verify logic before adding
'use client'. - Understanding Boundaries:
'use client'is the bridge between server logic and browser interactivity. - Layout Persistence: Layouts do not re-render on navigation, which saves performance but requires understanding for state management.
- The RSC Payload: Navigation is a soft merge of server data into the client DOM, not a hard page load.
- Caching is Aggressive: Learn
revalidatePathanddynamicconfigs to control data freshness. - File Conventions: Use
loading.tsxanderror.tsxto handle UI states declaratively.