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.
// 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.
// 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.
// 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.


