Next.js App Router: Patterns That Scale

Lessons from migrating large Next.js codebases — server components, caching, and streaming in practice.

The App Router is no longer experimental — it is the default for new Next.js apps and, increasingly, the answer for existing ones too. We have migrated four production codebases this year. Here are the patterns that held up and the pitfalls that caught us.

Server components are the mental shift

The biggest productivity win from the App Router comes from not having to build an API layer for read-only data. Fetch directly from your database in a server component. Your UI code becomes less code.

Default to server components. Add "use client" only where you need state, effects, or browser APIs.

Data fetching patterns that work

  • Parallel data: call Promise.all in a server component to fetch in parallel.
  • Sequential data: when a query depends on another, split into nested components and let Suspense stream them in.
  • Per-route data: put fetches in the route segment that uses them — not in layouts — so you do not break on navigation.

Caching: the part that bites everyone

Next.js caches aggressively by default. That is fine for marketing pages and painful for dashboards. The mental model we use:

  1. For data that changes often, export export const dynamic = "force-dynamic".
  2. For data that changes occasionally, use revalidate with a sensible window.
  3. For data that changes on user action, use revalidateTag or revalidatePath after the mutation.

Streaming and Suspense

Wrap slow server components in <Suspense> with a skeleton fallback. Users see the shell instantly, and slow data streams in as it arrives. This is the App Router's biggest perceived-performance win.

Server actions for mutations

Server actions replace most internal API routes for form submissions and mutations. Pair them with revalidateTag for cache invalidation. Avoid them for anything public or untrusted — stick to API routes there.

What does not work well yet

  • Very complex global state — client components with Zustand/Jotai still have a place.
  • Libraries that assume a browser environment in render (some chart libraries, some auth providers).
  • Middleware with heavy logic — keep it lean or performance suffers on every request.

Our migration playbook

Do not big-bang a migration. Move one route at a time, starting with read-heavy pages that benefit most from server components. Leave complex authenticated flows in Pages Router until you have a clear plan for auth and middleware.

← Back to blog

Need Help With A Next.js Project?

Whether it is a greenfield build or a migration from Pages Router, we can help.

Talk To Our Team