← All Articles
Engineering10 min read

Next.js 15 App Router: 6 Lessons From 12 Months in Production

March 30, 202510 min read

The Next.js App Router is a genuine step forward, but moving real applications onto it surfaces lessons the documentation does not emphasise. The mental model is different enough from the older approach that teams trip over the same things. Here is what we have learned shipping production applications on it, the kind we build through our web engineering work.

Server components are the default for a reason

In the App Router, components render on the server unless you opt out. Embracing this rather than fighting it is the biggest mindset shift. Keep data fetching and heavy logic on the server, and reach for client components only where you genuinely need interactivity. Teams that mark everything as a client component throw away most of the benefit and ship far more JavaScript than they need, which hurts Core Web Vitals.

Caching is powerful and surprising

The framework caches aggressively at several layers, and the single most common source of confusion is data that seems stale because it was cached more eagerly than expected. The fix is not to disable caching everywhere but to understand the layers and to be explicit about what should revalidate and when. Treat caching as something you configure deliberately, not something that should be invisible.

Streaming changes how pages feel

The App Router can stream a page in pieces, showing a meaningful shell immediately while slower data loads behind a boundary. Used well, this makes pages feel dramatically faster because users see structure right away. The lesson is to place loading boundaries thoughtfully around genuinely slow sections rather than wrapping everything or nothing.

Keep the server and client boundary clean

The line between server and client code is real, and crossing it carelessly causes confusing errors. Be intentional about what runs where, keep secrets and heavy dependencies strictly on the server, and pass only serialisable data across the boundary. Strong typing, in the spirit of our TypeScript patterns, makes this boundary much safer to work with.

Server actions are useful, not magic

Server actions simplify mutations by letting you call server code directly, but they are still endpoints. They need the same authentication, authorisation, and input validation as any API, because the convenience of the syntax does not change the security model. Treat them with the same caution you would any server entry point.

Common migration mistakes

Two patterns cause most of the pain when teams move an existing application across. The first is porting old data-fetching habits directly, wrapping everything in client-side effects instead of letting server components fetch data where it renders. The result is an app that technically uses the new router while keeping all the downsides of the old one. The second is treating every shared layout as static when parts of it genuinely need per-request data, which leads to confusing stale content. The fix in both cases is to start from the server, decide deliberately what must be dynamic, and push interactivity to the smallest possible client islands. Migrating incrementally, one route at a time, beats a big-bang rewrite, because it lets the team build an accurate mental model on low-risk pages before touching the critical ones.

Adopt it deliberately

The App Router rewards teams who learn its model and frustrates teams who treat it as a drop-in replacement for the old one. Invest the time to understand server components, caching, and streaming before committing a large application, and the payoff in performance and structure is real. If you want it adopted properly on a production application, our web engineering team has done it repeatedly.

GET STARTED

Ready to build
something exceptional?

From idea to launch in weeks, not months. Let's talk about your project.