Back to Blog
EngineeringNext.jsReactApp RouterRSC

Next.js 15 App Router: What We Learned Building 6 Production Apps

React Server Components, streaming, Partial Pre-Rendering — we've shipped six production applications with the App Router. Here's what we learned.

S

Samuel Spink

Founder & CEO

January 10, 202512 min read
N

Editorial focus

This article is written as an operator's guide: practical patterns, tradeoffs that matter in production, and decisions teams can apply without translating theory into action first.

The App Router is not simply a new way to organize files. It changes how teams think about rendering, data flow, caching, and responsibility boundaries across the entire application. That makes it powerful, but it also means many early migration problems are really architecture problems wearing framework clothes.

After building multiple production apps with the App Router, our conclusion is clear: it delivers real advantages when teams embrace the mental model fully. It creates confusion when teams try to reproduce old Pages Router habits with new primitives.

The biggest mindset shift is defaulting to the server

The App Router works best when server rendering is the baseline and client execution is opted into deliberately. Teams that keep most logic on the server usually end up with smaller bundles, simpler data flows, and fewer hydration issues.

The inverse is also true. If large sections of the tree become client components by default, the application often inherits the complexity of a traditional client-rendered React app without getting the full benefit of the App Router model.

React Server Components are a boundary tool

RSC is easiest to understand when treated as a boundary mechanism. Server components are ideal for reading data, assembling stable UI, and keeping non-interactive logic out of the client bundle. Client components are ideal for interaction, browser APIs, animation state, and local workflow management.

Problems usually start when that boundary is blurry. Examples include fetching the same data in multiple layers, passing large serialized objects into client components, or turning layout shells into client components just to support one small interaction.

The practical rule is simple: keep the client surface area as small as possible without making interactivity awkward.

Route structure matters more than teams expect

Nested layouts, parallel routes, and route groups provide significant leverage, but they reward intentional information architecture. When the route tree mirrors product structure cleanly, state retention and shared layouts feel elegant. When the tree grows organically without clear rules, debugging routing behavior becomes expensive.

We have found three habits especially useful:

  • Keep route groups meaningful and sparse.
  • Use shared layouts for true structural continuity, not convenience wrappers.
  • Make loading and error states part of route design from the start, not bolt-ons later.

This discipline keeps navigation behavior predictable and makes complex product areas easier to reason about.

Streaming improves perception when content is prioritized correctly

Streaming is valuable because it lets the user see progress earlier, but it is not automatically good UX. If the wrong parts of the page arrive first, the interface can feel fragmented. Strong streaming experiences reveal the stable frame and the most decision-useful content early, while lower-priority regions continue resolving.

That usually means:

  • The shell, headline, and primary actions arrive immediately.
  • Secondary analytics, recommendations, and dense tables stream later.
  • Skeletons match final layout closely enough to avoid shift.

Streaming is therefore a product-ordering problem as much as a rendering feature.

Caching is where many teams lose confidence

The App Router gives teams powerful caching tools, but the defaults can feel opaque if the data strategy is unclear. Confusion often comes from mixing highly dynamic and mostly static concerns in the same route segment.

The fix is not to disable caching everywhere. It is to decide, route by route, what freshness the user actually needs. Marketing pages, documentation, and stable product content can usually lean heavily on caching. Account dashboards, pricing states, and time-sensitive operational data need more deliberate invalidation or no-store behavior.

Once these rules are explicit, the model becomes much easier to trust.

Server Actions are useful, but only with restraint

Server Actions can reduce boilerplate for mutations and form submissions, especially in smaller products or focused internal tools. They are less effective when teams treat them as a reason to stop designing explicit application services.

Our preferred pattern is to let actions stay thin. They validate inputs, call a domain or application layer, and return a clear result. This preserves testability and keeps the framework-specific boundary narrow.

The integration story is strong when boundaries stay clean

In production work, the App Router often sits alongside authentication providers, payment systems, CMS platforms, analytics, and background jobs. The framework handles this well as long as external concerns are kept behind adapters and the route layer remains orchestration-focused.

Teams run into trouble when SDK details leak deep into component trees or when provider-specific shapes become the de facto application model. That creates migration pain later and makes tests harder to write with confidence.

What improved most in team workflows

The best outcomes we have seen are not only technical. The App Router can improve collaboration because responsibilities become clearer:

  • Designers and frontend engineers can reason about loading, empty, and error states at the route level.
  • Backend and product engineers can move more data logic server-side where it belongs.
  • Performance work becomes easier because client boundaries are explicit and reviewable.

That clarity matters. Framework features are only valuable insofar as they make teams better at shipping product.

The mistakes worth avoiding early

Across multiple apps, the most expensive mistakes were consistent:

  1. 1Converting too much of the tree to client components.
  2. 2Treating caching rules as incidental rather than designed.
  3. 3Using layouts to hide structure that should have been explicit.
  4. 4Mixing domain logic into route handlers and component files.
  5. 5Delaying error-state design until integration exposed failures.

Most of these are fixable, but they become more expensive once features pile on.

Our overall conclusion

Next.js 15 with the App Router is a strong production choice for teams that want modern React capabilities with disciplined server-first architecture. It rewards clarity. It punishes ambiguity. When used well, it improves performance, reduces client complexity, and creates a cleaner separation between presentation and application logic.

That is why we keep using it. Not because it is fashionable, but because it provides a solid operating model for the kinds of high-quality web products we build.

Share this article

Get insights in your inbox

Subscribe for engineering and design articles. No spam, unsubscribe anytime.