Back to blog
Next.jsReact

Next.js App Router: Patterns That Actually Work

07 Feb 20267 min read

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} />
  );
}
If this helped you, leave a ❤️

Comments

Coming Soon

Comment section will be available shortly

Have thoughts on this? Reach out — I'd love to chat.

Get in Touch