Next.js App Router: Patterns That Actually Work
Introduction
The App Router landed with Next.js 13 and became stable in 14. By now, enough production applications have been built with it that we can talk honestly about what works, what doesn't, and which patterns you'll wish you'd known at the start.
I've been building with the App Router since its early days — on this portfolio site and on several client projects. The mental model shift from the Pages Router is real and significant. But once it clicks, the primitives are genuinely powerful. The problem is that most tutorials either cover toy examples or dive into conceptual RSC theory without grounding it in the patterns you'll actually reach for every week.
This post is the practical guide I wish existed when I started.
Core Concepts
Server Components Are the Default — and That Matters
In the App Router, every component in app/ is a React Server Component (RSC) by default. RSCs render on the server, never ship their component code to the client, and can be async — meaning you can fetch data directly inside the component without hooks or useEffect:
// app/blog/page.tsx — this runs on the server
export default async function BlogPage() {
const posts = await fetchPosts(); // direct DB or API call
return (
<main>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</main>
);
}No API route needed. No useEffect. No loading state for initial data. This is not a minor DX improvement — it fundamentally changes how you architect data fetching.
The flip side: RSCs cannot use useState, useEffect, event handlers, or browser APIs. The moment you need any of those, you add 'use client' at the top of the file.
The Server/Client Boundary
The boundary between server and client isn't a hard wall — it's a waterfall you control. You push the 'use client' boundary as far down the component tree as possible:
app/
├── page.tsx ← Server Component (fetches data)
│ └── PostList ← Server Component (pure rendering)
│ └── LikeButton ← 'use client' (needs onClick)The LikeButton is client-side, but PostList and page.tsx remain on the server. This means the majority of your JavaScript never ships to the browser — only the interactive leaves of your component tree do.
A common mistake is marking entire page components as 'use client' just because one child needs interactivity. Don't. Extract the interactive parts, keep everything else on the server.
Practical Examples
Parallel Data Fetching
export default async function DashboardPage() {
const [user, stats, recentActivity] = await Promise.all([
fetchUser(),
fetchStats(),
fetchRecentActivity(),
]);
return (
<Dashboard user={user} stats={stats} activity={recentActivity} />
);
}