FrontendSeptember 15, 20258 min read

Understanding Next.js App Router

The App Router isn't just a new folder structure - it's a fundamentally different mental model. Here's what server components, client components, and streaming actually mean for how you build.

Understanding Next.js App Router

If you've been building Next.js apps with the pages/ directory and suddenly switched to app/, you've probably run into confusing errors, unexpected behavior, and the haunting question: why is 'use client' everywhere in my codebase? This article is my attempt to give you the mental model that makes the App Router click.

# The Core Idea: Two Worlds, One App

In the App Router, every component lives in one of two worlds: the server or the client. Server Components run on the server, never touch the browser, and produce HTML. Client Components run in the browser and handle interactivity. The critical insight: Server Components are the default. You opt into the client world by writing 'use client' at the top of a file.

🧠

The most common mistake: adding 'use client' to everything 'just to be safe'. This defeats the entire purpose of the App Router and ships unnecessary JavaScript to the browser.

# Server Components: What They Can (and Can't) Do

Server Components can directly query databases, read files, access environment variables, and use heavy server-only libraries - without any of that code ever reaching the browser. No API routes needed for data fetching in layouts and pages.

typescript
// app/dashboard/page.tsx - this is a Server Component by default
// No 'use client' = runs on server, never shipped to browser

import { db } from "@/lib/db";

export default async function DashboardPage() {
  // Direct database query - no fetch, no API route, no useEffect
  const user = await db.user.findUnique({
    where: { id: getCurrentUserId() },
    select: { name: true, email: true, plan: true },
  });

  return (
    <div>
      <h1>Welcome back, {user.name}</h1>
      <PlanBadge plan={user.plan} /> {/* Can be a Server Component too */}
    </div>
  );
}

What they can't do: useState, useEffect, onClick handlers, browser APIs (window, localStorage), or anything that requires the component to be interactive after it loads. If you need those, you need a Client Component.

# Client Components: Use Them Surgically

The golden rule: push Client Components to the leaves of your component tree. A navbar with a mobile menu toggle? That's a Client Component. The page layout that renders it? Server Component. The blog content inside the page? Server Component.

typescript
// components/MobileMenu.tsx - only this small piece needs to be client-side
"use client";
import { useState } from "react";

export function MobileMenu({ links }: { links: NavLink[] }) {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(!open)}>Menu</button>
      {open && <NavLinks links={links} />}
    </>
  );
}

// app/layout.tsx - the layout itself stays a Server Component
// It imports MobileMenu (client) and everything else (server)
import { MobileMenu } from "@/components/MobileMenu";

export default function Layout({ children }) {
  const links = getNavLinks(); // server-side, no API needed
  return (
    <html>
      <body>
        <header>
          <MobileMenu links={links} /> {/* client island inside server layout */}
        </header>
        {children}
      </body>
    </html>
  );
}

# Streaming with Suspense: Show Something Fast

Streaming lets you send HTML to the browser in chunks. Wrap slow data-fetching components in Suspense and the rest of your page renders immediately while they load. This is what makes Next.js apps feel instant even when backend calls are slow.

typescript
// app/dashboard/page.tsx
import { Suspense } from "react";
import { UserStats } from "./UserStats";       // fast
import { RecentOrders } from "./RecentOrders"; // slow DB query
import { Skeleton } from "@/components/Skeleton";

export default function DashboardPage() {
  return (
    <div>
      <UserStats />  {/* renders immediately */}
      
      <Suspense fallback={<Skeleton rows={5} />}>
        <RecentOrders />  {/* streams in when ready */}
      </Suspense>
    </div>
  );
}

# The File System Is Your Router

In the App Router, folders define routes and special files define behavior. The conventions you need to know:

  • `page.tsx` - the UI for a route. Creates a publicly accessible URL.
  • `layout.tsx` - wraps a route and all its children. Persists across navigation (great for sidebars and navbars).
  • `loading.tsx` - automatic Suspense boundary. Shows while the page fetches data.
  • `error.tsx` - error boundary for a route segment. Catches thrown errors gracefully.
  • `not-found.tsx` - renders when `notFound()` is called or a route doesn't exist.
  • `route.ts` - creates an API endpoint. Replaces `pages/api/` files.

The App Router is more powerful than the Pages Router - but it requires a different mental model. Once you internalize 'server by default, client by exception', everything becomes cleaner, faster, and more intuitive.

Written by

Muhiu Din

Full-Stack Engineer

More Articles