After shipping more than a dozen Next.js 15 App Router projects in production, we have a set of patterns that work and a list of pitfalls that cost real days. Here is the short version.
Understand the caching model first
The App Router caches aggressively across four layers. Most confusing bugs trace back to a misunderstanding of when data is cached and how to opt out. Learn the caching model before you write a line of data-fetching code.
The four layers are worth naming, because each has its own rules:
- Request memoisation — identical fetches within one render are made once.
- The data cache — fetch results stored on the server across requests.
- The full route cache — the rendered output of a route, stored on the server.
- The router cache — route segments kept in the browser as the user navigates.
When a page shows old data, the first question is which of these four is holding it. Decide per route whether it should be static, revalidated on a schedule or rendered on every request, and write that decision down in the code.
Server components change how you fetch
Fetch data where it is rendered, keep client components small, and pass serialisable props down. Fighting the server-first model leads to hydration errors and duplicated logic.
A useful rule: a component is a server component until it needs state, an effect or a browser event. Then only that small interactive part becomes a client component. Marking a whole page as a client component to fix one button sends far more JavaScript to the browser than needed.
Patterns we standardised
- A shared data-access layer so caching rules live in one place.
- Strict boundaries between server and client components.
- A boilerplate that saves roughly two days of setup per project.
Pitfalls that cost us time
- Request waterfalls. Awaiting one fetch after another when they could run together makes pages slow for no reason. Start independent requests in parallel.
- Stale pages after a change. If an update does not revalidate the paths or tags it affects, users see old data. Revalidation belongs next to every write.
- Secrets in the wrong place. Code imported into a client component ships to the browser. Keep server-only modules clearly separated.
- Hydration mismatches. Anything that differs between server and browser, such as dates, random values or browser-only APIs, must be handled deliberately.
- Treating server actions as trusted. A server action is a public endpoint. Validate input and check permissions inside every one.
Streaming and loading states
Streaming lets the page shell appear immediately while slower sections load. Wrap the slow parts in suspense boundaries with a sensible placeholder, so one slow query does not hold back the whole page. It is one of the biggest improvements in how fast a site feels, and it costs very little to adopt.
Migrating from the Pages Router
The two routers can run in the same project, so migrate route by route. Start with a simple page, establish your data-access layer and component boundaries there, then move the rest. A single big-bang migration is rarely worth the risk.
Building a fast, modern web app? See our web development services, the stack we use, or hire dedicated React and Next.js developers.