Next.js — Apr 10, 2025 — 7 min read
Next.js App Router Decisions I'd Change on My Last SaaS Build
A SaaS dashboard built on Next.js 14 App Router. Six months in production. Three architectural decisions I would reverse.

Six months ago I shipped a SaaS dashboard on Next.js 14 with the App Router. The App Router was not new, but it was the first project where I committed to it fully — no Pages Router fallback, no hybrid routing. Server Components everywhere I could justify them.
Three of those decisions are wrong in ways that were not obvious until we were in month four.
Decision 1: Server Components for too much UI
The App Router makes Server Components the default, and the mental model makes them feel correct for anything that fetches data. I pushed that logic too far.
Dashboard panels that fetch a user's recent activity. Settings forms that need current values pre-populated. Sidebar navigation that shows the active workspace name. All of these ended up as Server Components fetching directly from the database via Supabase's server client.
The first problem is waterfall rendering. Each Server Component that fetches independently adds to the initial response time in a way that is hard to see in development — where the database is fast and local — and very visible in production with a real-latency Supabase instance.
The second problem is invalidation. Server Components do not have a cache invalidation primitive that is ergonomic for interactive applications. When the user updates their workspace name, the sidebar still shows the old one until a full page navigation. router.refresh() is the escape hatch, but calling it after every mutation from a Client Component defeats the architecture you built.
What I would do differently: fetch the initial data in one Server Component at the route level, pass it down as props, and let Client Components own the mutations and optimistic updates. The architecture is less novel. It is more correct.
Decision 2: Layout nesting depth
App Router layouts are composable, which led me to compose them aggressively. The final structure was a root layout wrapping an auth layout wrapping a dashboard layout wrapping a workspace layout wrapping a settings layout.
Five levels. Each one with its own async data fetch, its own Suspense boundary, its own error boundary.
The consequence: any change to the workspace context — switching workspaces, for example — required invalidating and re-rendering layouts three and four simultaneously. The loading states bled into each other. The user saw a partial flash of the previous workspace's data before the new workspace's data resolved.
Two layout levels handle 90% of what real SaaS applications need: one for unauthenticated pages, one for authenticated pages. Everything else is a page-level concern. I spent two weeks unwinding the layout nesting.
Decision 3: Server Actions for mutations that needed optimism
Server Actions were the obvious choice for form submissions and data mutations. They co-locate with Server Components, they handle progressive enhancement, and they make the code look clean.
What they do not do well is optimistic UI. An action that creates a record and then reflects that record in the UI has a visible round-trip delay on a standard connection. For a dashboard where users are creating records frequently — adding tasks, logging entries, sending messages — the lag is felt on every interaction.
The React useOptimistic hook is the prescribed solution. In practice, synchronizing the optimistic state with the Server Action's response requires more state management scaffolding than just using a client-side mutation with React Query or SWR and a manual cache update.
Server Actions are correct for low-frequency, high-stakes mutations: submitting a payment, deleting an account, changing a plan. For high-frequency interactive mutations in a dashboard, a typed API route with React Query gives you optimistic updates, cache invalidation, and retry logic that useOptimistic alone does not.
What held up
Not all of it needs changing. Route-level data fetching with async/await directly in Server Components is genuinely better than getServerSideProps. Parallel routes for modals are cleaner than the Portal patterns they replace. The metadata API is the best SEO DX I have used in any framework.
These are not reasons to avoid the App Router. They are reasons to be more conservative about where you push its newest patterns.
Conclusion
The App Router is the right foundation for new Next.js projects. The mistake is treating its defaults as prescriptions rather than starting points. Server Components, layout nesting, and Server Actions all have correct scopes. Expanding those scopes because the architecture permits it is how you get a rewrite in month four.
Summary
Do not push Server Components below the route level for interactive dashboard UI — waterfall fetching and stale post-mutation state follow. Keep layout nesting to two levels; deeper hierarchies create invalidation and loading-state collisions. Use Server Actions for low-frequency mutations only; high-frequency interactive mutations need React Query or SWR with optimistic updates. The parts that held up — route-level async fetching, parallel routes, and the metadata API — are worth building on.



