Why We Went All-in on App Router
When Next.js 13 shipped the App Router as stable, we made a deliberate decision to migrate all new projects immediately. The promise was compelling: server components by default, nested layouts, streaming, and co-located data fetching. After 12 months and 8 production apps later, here's what we learned.
Lesson 1: Server Components Are Not Just About Performance
The mental model shift is bigger than most tutorials convey. Server components don't just "run on the server for speed" — they fundamentally change how you think about data flow. You no longer have a client-side request waterfall; instead you have a render tree where every node can independently fetch its own data.
The practical implication: stop lifting state to the top. Let each component own its data. This makes codebases dramatically easier to reason about.
Lesson 2: The Fetch Cache Will Bite You
Next.js extends native fetch with automatic caching. By default, every fetch in a server component is cached indefinitely. This is great for static data and catastrophic if you forget about it.
// This is CACHED forever — probably not what you want for user data
const data = await fetch('/api/user-profile');
// This is correct for dynamic user data
const data = await fetch('/api/user-profile', { cache: 'no-store' });
// This revalidates every 60 seconds
const data = await fetch('/api/posts', { next: { revalidate: 60 } });We've had staging environments serving stale data for hours because a developer forgot cache: 'no-store'.
Lesson 3: `use client` Boundaries Are Expensive — Plan Them Early
Every "use client" directive creates a new JavaScript bundle entry point. If you put it at the top of a large component tree, you're shipping all those children as client-side JS.
The pattern that works: keep your leaf interactive nodes as client components. Keep containers and layouts as server components. Draw your boundaries on a whiteboard before writing a single line of code.
Lesson 4: Parallel Routes Are Underused
The @slot convention for parallel routes unlocks genuinely complex UX patterns — modals that don't unmount the background, split-view dashboards, and intercepted routes for lightboxes. Most teams ignore this feature and reach for client-side state management instead.
Lesson 5: Streaming + Suspense is the Real Win
Streaming with Suspense allows meaningful content to appear before heavy data loads complete. Wrap slow components in with a skeleton fallback and your perceived performance improves dramatically — no code changes required on the data fetching side.
Lesson 6: TypeScript Path Aliases Save Sanity
In a large App Router project you quickly get ../../../../components/ui/Button. Set up path aliases in tsconfig.json from day one:
{
"paths": {
"@/components/*": ["./components/*"],
"@/lib/*": ["./lib/*"],
"@/app/*": ["./app/*"]
}
}Lesson 7: Middleware is Powerful but Runs on Every Request
Next.js Middleware runs on the Edge before every request. It's tempting to put authentication checks, A/B testing, geolocation, and feature flags all in middleware. Resist this. Every millisecond of middleware is added to every page load. Keep it focused on auth redirects and locale detection only.
Lesson 8: Testing Server Components Is Still Awkward
The ecosystem hasn't fully caught up. Vitest + React Testing Library can test client components fine. Server components require a different approach — we've had the most success with Playwright for integration tests rather than unit testing server components directly.
Lesson 9: ISR Is Your Best Friend for Content Sites
Incremental Static Regeneration with revalidate hits the sweet spot between static performance and fresh content. For our e-commerce clients, product pages set to revalidate: 30 get static-site-level Lighthouse scores with near-real-time inventory accuracy.
Lesson 10: The Ecosystem Is Catching Up Fast
12 months ago, many third-party libraries broke inside server components. Today, nearly everything works. The biggest remaining pain point is CSS-in-JS libraries that rely on runtime context — they still require client component wrappers.
The Verdict
App Router is genuinely better for complex applications. The learning curve is real and the mental model shift is significant. But teams that invest in understanding server/client boundaries build faster, more performant apps with less client-side state management complexity. We're not going back.
