Next.js 14 App Router: A Practical Guide
A hands-on guide to the Next.js 14 App Router: server components, data fetching, caching, and when to reach for a client component.
The App Router is no longer the new kid. As of Next.js 14 it's the default, it's stable, and it's how most US teams start new projects. But the mental model is genuinely different from the Pages Router, and that's where people trip up. Here's what actually matters day to day.
Server components are the default
Every component under app/ is a React Server Component unless you say otherwise. That means it runs on the server, never ships to the browser, and can talk directly to your database or filesystem.
// app/posts/page.tsx — runs on the server
import { db } from '@/lib/db';
export default async function PostsPage() {
const posts = await db.post.findMany();
return <PostList posts={posts} />;
}
No useEffect, no loading spinner boilerplate, no API route in the middle. You await your data right in the component.
When you actually need a client component
Reach for 'use client' only when you need interactivity or browser APIs:
- State and effects (
useState,useEffect) - Event handlers (
onClick,onChange) - Browser-only APIs (
localStorage,window) - Third-party libraries that use any of the above
The trick most people miss: keep client components small and push them to the leaves of your tree. A single interactive button doesn't require the whole page to be a client component.
Server Components vs Client Components
| Concern | Server Component | Client Component |
|---|---|---|
| Runs where | Server only | Server (initial) + browser |
| Ships JS to browser | No | Yes |
| Data fetching | Direct await | fetch in effect or a lib |
| State / interactivity | No | Yes |
| Access to DB / secrets | Yes | No |
| Marked with | (default) | 'use client' |
Data fetching and caching
In the App Router, fetch is extended with caching controls. This is the part that changed most in the Next.js 14 era, so read the labels carefully:
fetch(url)— cached by default (static)fetch(url, { cache: 'no-store' })— always fresh (dynamic)fetch(url, { next: { revalidate: 3600 } })— ISR: revalidate every hour
For content sites, revalidate is your friend. You get static-fast pages that still pick up new content without a full redeploy.
A few rules that save you pain
- Don't import server-only code into client components. Use the
server-onlypackage to enforce it. - Colocate
loading.tsxanderror.tsxwith your routes for automatic Suspense and error boundaries. - Use
generateStaticParamsfor dynamic routes you want pre-rendered at build time.
FAQ
Do I have to migrate my Pages Router app?
No. The two routers work side by side in the same project. Migrate route by route when it makes sense, not all at once.
Are Server Components slower because they run on the server?
Usually the opposite. They ship less JavaScript, so the browser has less to parse and execute. The server work is fast and cacheable.
Can I use my favorite data-fetching library like React Query?
Yes, inside client components. But for most server-rendered data you won't need it — a direct await is simpler.
How do I share state across server and client components?
You don't share React state across that boundary. Pass data down as props from server to client, and lift interactive state into the client subtree.
The verdict
If you're starting a new project in 2026, use the App Router. The server-first model removes a whole category of boilerplate, and the caching controls give you fine-grained performance without ejecting to custom infrastructure. Keep client components small, lean on revalidate for content, and you'll ship faster with less JavaScript.
Jordan is a full-stack engineer with over a decade of experience shipping production web applications for US startups and enterprises. He specializes in the React/Next.js ecosystem, serverless architectures on Vercel, and Postgres-backed products on Supabase. He founded this site to cut through marketing noise and test developer tools against real workflows.
More from Jordan Alvarez →Related Reading
Supabase Auth with Next.js: A Setup Tutorial
Step-by-step tutorial for adding Supabase authentication to a Next.js App Router app, including sessions, protected routes, and RLS.