Skip to content

Next.js App Router — Modern SSR with React Server Components

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Next.js App Router. We cover key concepts, practical examples, and best practices to help you master this topic.

Next.js App Router uses React Server Components for SSR, enabling server-side data fetching without sending JavaScript to the client, automatic streaming with Suspense, and nested layouts.

What You'll Learn

By the end of this tutorial, you will understand how the Next.js App Router differs from the Pages Router, how React Server Components work, how to fetch data in Server Components, how to use layouts and templates, and how streaming SSR improves perceived performance.

Why It Matters

The App Router represents the future of React SSR. React Server Components allow you to fetch data and render components on the server without sending their JavaScript to the client. This reduces bundle sizes, improves performance, and simplifies data fetching. The App Router is the recommended approach for new Next.js projects.

Real-World Use

A SaaS dashboard rebuilt with Next.js App Router reduced their client-side JavaScript by 40 percent by moving data-heavy components to Server Components. The analytics page, which previously sent a 200KB charting library to the client, now renders the chart as static HTML on the server and streams interactive elements on demand.

App Router Architecture
    ┌──────────────────────────────────────────────────────────┐
    │              Next.js App Router                          │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  app/                                                    │
    │  ├── layout.js     (shared layout for all routes)       │
    │  ├── page.js       (home page — Server Component)       │
    │  ├── about/                                             │
    │  │   ├── page.js   (/about — Server Component)          │
    │  │   └── layout.js (/about specific layout)             │
    │  ├── products/                                           │
    │  │   ├── page.js   (/products — Server Component)       │
    │  │   └── [id]/                                           │
    │  │       └── page.js (/products/123 — dynamic)         │
    │  ├── api/                                                │
    │  │   └── route.js  (API route)                          │
    │  └── loading.js    (loading UI for Suspense)            │
    │                                                          │
    │  Server Components: fetch data, render HTML              │
    │  Client Components: interactivity (add 'use client')    │
    │  Layout: persists across navigations                    │
    │  Loading: shown while page streams                      │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of the App Router like a smart building with reusable floors. A layout is the building's lobby and hallways — they stay the same as you move between rooms. Server Components are pre-printed posters on the walls — fully visible without needing a projector (JavaScript). Client Components are interactive kiosks — they need power (JavaScript) to function but are only placed where visitors need them.

Server Component Data Fetching

// app/page.js — Server Component (default, no 'use client')
// This component runs on the server, fetches data, renders HTML
// NO JavaScript is sent to the client for this component

async function getProducts() {
    const res = await fetch('https://api.example.com/products', {
        // Next.js extends fetch with caching options
        cache: 'no-store'  // SSR — fetch on every request
        // cache: 'force-cache'  // SSG — fetch once at build time
        // next: { revalidate: 60 }  // ISR — revalidate every 60s
    });

    if (!res.ok) {
        throw new Error('Failed to fetch products');
    }

    return res.json();
}

export default async function HomePage() {
    // Direct async/await in the component — no hooks needed
    const products = await getProducts();

    return (
        <div>
            <h1>Product Catalog</h1>
            <div className="grid">
                {products.map(product => (
                    <ProductCard key={product.id} product={product} />
                ))}
            </div>
        </div>
    );
}

// Expected behavior:
// 1. Server fetches products from API
// 2. Server renders the HTML with product data
// 3. HTML sent to browser (immediately visible)
// 4. No JavaScript for this component
// 5. Zero client-side data fetching!

Client Components and Interactivity

// app/products/[id]/page.js — dynamic product page
// Parent can be a Server Component
import ProductDetails from './ProductDetails';
import AddToCartButton from './AddToCartButton';

async function getProduct(id) {
    const res = await fetch(`https://api.example.com/products/${id}`);
    return res.json();
}

export default async function ProductPage({ params }) {
    // Server Component — fetches data on server
    const product = await getProduct(params.id);

    return (
        <div>
            <ProductDetails product={product} />
            {/* Client Component — interactive */}
            <AddToCartButton productId={product.id} />
        </div>
    );
}

// app/products/[id]/AddToCartButton.js
// Mark as Client Component for interactivity
'use client';

import { useState } from 'react';

export default function AddToCartButton({ productId }) {
    const [added, setAdded] = useState(false);

    return (
        <button onClick={() => {
            setAdded(true);
            // API call to add to cart
        }}>
            {added ? 'Added to Cart!' : 'Add to Cart'}
        </button>
    );
}

// The ProductDetails component remains a Server Component
// No JavaScript sent for the product description, price, images
// Only the button sends JavaScript to the client

Layouts and Nested Routing

// app/layout.js — Root layout (wraps ALL pages)
export default function RootLayout({ children }) {
    return (
        <html lang="en">
            <body>
                <header>
                    <nav>
                        <a href="/">Home</a>
                        <a href="/products">Products</a>
                        <a href="/about">About</a>
                    </nav>
                </header>

                <main>{children}</main>

                <footer>
                    <p>Built with Next.js App Router</p>
                </footer>
            </body>
        </html>
    );
}

// app/products/layout.js — Products layout (wraps /products/*)
export default function ProductsLayout({ children }) {
    return (
        <section>
            <aside>
                <h2>Categories</h2>
                <CategoryList />
            </aside>
            <article>{children}</article>
        </section>
    );
}

// Layouts persist across navigations
// The products layout does NOT re-render when navigating between products
// Only the page content updates — layout stays mounted

// app/loading.js — Shown while page content streams
export default function Loading() {
    return (
        <div className="skeleton">
            <div className="skeleton-header" />
            <div className="skeleton-content" />
            <div className="skeleton-footer" />
        </div>
    );
}

// app/error.js — Error boundary for the route segment
'use client';

export default function Error({ error, reset }) {
    return (
        <div>
            <h2>Something went wrong!</h2>
            <button onClick={reset}>Try again</button>
        </div>
    );
}

Common Mistakes

  1. Using hooks in Server Components. Server Components cannot use useState, useEffect, or other React hooks. Only use these in Client Components marked with 'use client'.
  2. Making all components client components. The default is Server Component. Only add 'use client' when you need interactivity. Every client component increases bundle size.
  3. Not using the cache option for fetch. Next.js extends fetch with Caching. For SSR, use cache: 'no-store'. Default is 'force-cache' (SSG), which may serve stale data.
  4. Passing functions from Server to Client Components. You cannot pass functions, Date objects, or other non-serializable data from Server to Client Components. Only plain objects and primitives.
  5. Forgetting that layouts do not unmount between pages. Layout state persists. Do not put page-specific logic in layouts. Use layouts for shared UI only.

Practice Questions

  1. What is the difference between Server Components and Client Components?
  2. How does data fetching work in the App Router without hooks?
  3. What is the benefit of streaming SSR with loading.js?
  4. How do layouts work differently in the App Router vs Pages Router?
  5. When should you add 'use client' to a component?

Challenge: Build an e-commerce app with the App Router: a product listing page as a Server Component with async data fetching, a product detail page with Server Component for data and Client Component for the Add to Cart button, nested layouts (root + products), loading state with loading.js, error handling with error.js, and a search feature using searchParams.

FAQ

Should I use Pages Router or App Router for a new project?

Use App Router for new projects. It is the recommended approach and includes React Server Components, streaming, and improved performance. Pages Router is in maintenance mode.

Can I use App Router with existing Pages Router code?

Yes, you can incrementally adopt App Router. Both routers can coexist in the same project. Migrate one page at a time.

Do Server Components support TypeScript?

Yes. Server Components fully support TypeScript. You can use TypeScript in both Server and Client Components.

How do I handle authentication in App Router?

Read cookies or headers in Server Components using next/headers. Check authentication in a layout or middleware and redirect unauthenticated users.

Does App Router support middleware?

Yes. Create a middleware.ts file in the root of your project. Middleware runs before requests and can redirect, rewrite, or add headers based on cookies or geolocation.

Mini Project

Build a full-featured blog with Next.js App Router: root layout with navigation, dynamic blog post pages using Server Components with async data fetching from a CMS API, a search page using searchParams, loading skeletons with loading.js, error boundaries with error.js, a Client Component for the comment form, and middleware for redirecting old URLs.

What's Next

You understand App Router. Now learn about getServerSideProps for per-request data fetching in depth.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro