All posts
· Next.js· React· App Router· Web

Next.js App Router, Explained

The Next.js App Router changed the default: components now run on the server unless you opt out. That one shift explains most of what feels new — layouts, data fetching, streaming, and where your JavaScript actually runs.

Here's the mental model I use when building with it.

Server Components are the default

Every file under app/ is a Server Component until you add "use client". Server Components render on the server and send HTML — their code never ships to the browser. That means you can fetch data directly inside them:

export default async function Page() {
  const posts = await getPosts(); // runs on the server
  return <PostList posts={posts} />;
}

No useEffect, no loading spinner boilerplate, no exposing API keys to the client. The data fetch happens where the data lives.

Reach for a Client Component only when you need the browser

Add "use client" when a component needs something that only exists in the browser:

  • State and effects (useState, useEffect)
  • Event handlers (onClick, onChange)
  • Browser APIs (window, localStorage, WebGL, animation libraries)

The goal is a small island of interactivity inside a mostly-server tree. Keep "use client" at the leaves, not the root, so you ship as little JavaScript as possible.

Layouts nest and persist

layout.tsx wraps every route in its folder, and layouts don't re-render on navigation between their children. That's why the App Router is great for persistent chrome — a navbar, a sidebar, a smooth-scroll provider — that should survive page changes without remounting.

app/
  layout.tsx        → wraps everything
  blog/
    layout.tsx      → wraps every /blog route
    page.tsx        → /blog
    [slug]/page.tsx → /blog/:slug

Metadata is first-class

Instead of hand-writing <head> tags, you export a metadata object (or a generateMetadata function for dynamic routes). Next merges it down the tree, so a layout can set site-wide defaults and each page overrides just what it needs — titles, canonical URLs, Open Graph, all typed.

Static export still works

Even with all of this, you can ship a fully static site with output: "export". Server Components render at build time, generateStaticParams enumerates your dynamic routes, and you get plain HTML you can host anywhere — including GitHub Pages. This very blog is built that way.

Takeaway

Think server-first: fetch where the data lives, keep client components as small islands, let layouts hold the persistent shell. Once "server by default" clicks, the rest of the App Router stops feeling like magic and starts feeling like the obvious way to build.

Mujtaba Chandio AI Engineer