Back to Blog
Deep Dive: How the Next.js App Router Actually Works
frontendnextjsarchitecture

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

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" />:

  1. Next.js does not fetch a full HTML document.
  2. It fetches the RSC Payload (a special JSON-like format).
  3. 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:

  1. 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
  1. 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 Boundary
  • not-found.tsx: Handles 404s
  • layout.tsx: Wraps the children

Summary

  1. Think Server-First: Components are server-side by default; verify logic before adding 'use client'.
  2. Understanding Boundaries: 'use client' is the bridge between server logic and browser interactivity.
  3. Layout Persistence: Layouts do not re-render on navigation, which saves performance but requires understanding for state management.
  4. The RSC Payload: Navigation is a soft merge of server data into the client DOM, not a hard page load.
  5. Caching is Aggressive: Learn revalidatePath and dynamic configs to control data freshness.
  6. File Conventions: Use loading.tsx and error.tsx to handle UI states declaratively.

Design & Developed by AkshayMoolya
© 2026. All rights reserved.