I've shipped four production applications using Next.js App Router over the past year: an invoicing SaaS, a real estate platform, an automotive ERP, and a CRM system. Here's what I actually learned — not the tutorial version, the production version.
Server Components Are the Real Deal
The hype is justified, but not for the reasons most articles cite. Yes, smaller bundle sizes matter. But the real win is architectural clarity.
With Server Components, I finally have a clean answer to "where does this data come from?" If a component is a Server Component, its data comes from the server at render time. Period. No useEffect waterfalls, no loading spinners for data that should have been there on first paint.
In Eqidis, the invoice list page is a Server Component. It queries the database, renders the table, and sends HTML. Zero client-side JavaScript for the initial view. The interactive filters? Those are Client Components that handle just the filtering logic.
The Patterns That Survived Production
1. The "Smart Layout, Dumb Page" Pattern
app/
dashboard/
layout.tsx ← Auth check, sidebar, nav (Server Component)
page.tsx ← Just renders the content (Server Component)
invoices/
page.tsx ← Fetches and renders invoice list
[id]/
page.tsx ← Single invoice viewLayouts handle cross-cutting concerns. Pages are simple. This scales beautifully because adding a new page means adding ONE file that does ONE thing.
2. Colocated Server Actions
Instead of building separate API routes, I put Server Actions right next to the components that use them:
components/
invoice-form/
form.tsx ← Client Component with the form UI
actions.ts ← Server Actions: createInvoice, updateInvoice
validation.ts ← Zod schemas shared between client and serverThis colocation is powerful. When you need to change how invoices are created, everything is in one folder.
3. Optimistic Updates for Everything User-Facing
Users don't care about your server round-trip time. When someone clicks "Mark as Paid" on an invoice, the UI should update instantly. Use useOptimistic for any action where you can predict the outcome.
What's Overhyped
Partial Prerendering
Cool concept, but in practice, most of my pages are either fully dynamic (dashboard, user-specific data) or fully static (marketing pages). The hybrid case is rarer than the docs suggest.
Route Handlers for Everything
I see projects creating route.ts files for every data mutation. If it's called from your own app, use a Server Action instead. Route Handlers are for external consumers (webhooks, third-party integrations).
What Actually Bit Me
1. Caching Complexity
Next.js caching is powerful but has multiple layers (Request Memoization, Data Cache, Full Route Cache). In production, I spent more time debugging "why isn't this data fresh?" than I'd like to admit. My rule now: start with no-store on dynamic data, then opt into caching explicitly.
2. Client/Server Boundary Confusion
In GraxiHome, I initially made the payment simulator a Server Component because "it fetches data." But it also needs real-time interactivity — sliders, instant recalculations. The fix was obvious in hindsight: fetch data in a Server Component parent, pass it as props to a Client Component child.
The rule: If it reacts to user input, it's a Client Component. If it just displays data, it can be a Server Component.
3. Error Boundaries Need More Love
The default error.tsx pattern works, but production errors need context. I now wrap every major section with a custom error boundary that logs the error, shows a retry button, and includes a "Report this issue" link.
My Current Next.js Stack
For every new project, I start with:
- - Next.js with App Router (obviously)
- - Tailwind CSS + shadcn/ui — consistent design, fast iteration
- - Zod for validation everywhere (forms, API inputs, env vars)
- - PostgreSQL with Drizzle ORM
- - Vercel for deployment (the DX is unmatched for Next.js)
The Bottom Line
App Router isn't perfect, but it's the best architecture I've used for building full-stack web applications. The key is understanding that it's a Server-first framework now. Start on the server, drop to the client only when you need interactivity.
Stop fighting the model. Embrace it, and you'll ship faster than ever.