After building over ten production projects with the Next.js App Router, I've accumulated a solid set of patterns that work and an equally long list of mistakes I won't repeat. This post is the article I wish I'd had when I first migrated from the Pages Router.
Server Components Are the Default — Embrace It
The single biggest mental shift is that components are server-rendered by default. This means you can fetch data directly inside any component without useEffect or client-side loading states. I now fetch data at the component level closest to where it's needed, keeping my pages lean. Only add 'use client' when you genuinely need interactivity — event handlers, hooks, browser APIs.
Data Fetching Patterns That Work
- Use fetch() with Next.js cache options for data that changes infrequently
- Use generateStaticParams for dynamic routes with finite known values
- Prefer parallel data fetching with Promise.all to avoid waterfall requests
- Move auth checks into middleware for consistent protection across routes
- Use React Suspense boundaries to progressively stream content to the client
Common Pitfalls
The most common mistake I see is importing server-only modules into client components. Next.js will throw a cryptic error that's hard to trace. Install the server-only package and import it at the top of any file that should never run in the browser. Another pitfall: don't put large client bundles in layout.tsx — that bundle will be sent on every page load. Keep layouts thin.
The App Router has a learning curve, but once it clicks you'll wonder how you lived without it. The combination of server components, streaming, and the new caching model makes Next.js apps faster and more maintainable than ever.