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.allin 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:
- For data that changes often, export
export const dynamic = "force-dynamic". - For data that changes occasionally, use
revalidatewith a sensible window. - For data that changes on user action, use
revalidateTagorrevalidatePathafter 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