Next.js App Router Pitfalls I Wish I Knew Three Months Ago
I have been building with the Next.js App Router for about a year. The first three months produced several productive weekends and several wasted ones. The pitfalls that cost me time tend not to be the ones the documentation highlights. They are the structural mismatches between the App Router’s conceptual model and the mental model most developers bring from earlier React work.
Pitfall one: not understanding the server-client boundary
The App Router defaults components to server components. A server component cannot use state, cannot use effects, cannot use browser APIs. Client components need an explicit “use client” directive.
The pitfall is mixing the two carelessly. A server component that imports a client component is fine. A client component that imports a server component is not. The boundary cascades downward through the import tree, and a single carelessly placed import can force a large subtree into client rendering when you intended server rendering.
The practical defence is to be deliberate about the directives at the top of each file. The other defence is to organise the code so that the server-client boundary is visible in the directory structure.
Pitfall two: data fetching in the wrong place
The App Router supports data fetching inside server components directly. You can await a fetch call in the function body of a component. This is the recommended pattern for most cases.
The pitfall is using the legacy patterns by reflex. useEffect data fetching in a client component still works but loses the benefits of the App Router approach. SWR or React Query in client components is sometimes the right answer but more often the right answer is to move the fetch to a server component.
The mental shift is to think about which data is needed at render time on the server versus which data is needed reactively in the client. Most data is the former, and the App Router lets you fetch it there directly.
Pitfall three: caching surprises
The App Router has multiple layers of caching. The Request Memoization layer dedupes fetch calls in a single request. The Data Cache layer persists fetch results across requests. The Full Route Cache layer caches the rendered HTML. The Router Cache caches in the browser.
The defaults are aggressive caching. The pitfall is that you expect a fetch to return fresh data and it returns cached data instead.
The defence is to be deliberate about cache directives. The fetch options accept cache and next.revalidate parameters. The route segment config accepts dynamic and revalidate options. Knowing which lever applies to which caching layer is the work.
Pitfall four: searchParams handling
The searchParams in a Page component are an async prop in modern Next.js. The await is required and the legacy synchronous pattern is deprecated. The pitfall is that the deprecation warning is easy to miss and the legacy pattern still works in development but breaks in production builds with specific Next.js versions.
The defence is to read the searchParams declaration in the docs every time you start a new project. The pattern has changed between versions and the documentation is the only reliable source.
Pitfall five: streaming and suspense interactions
The App Router supports streaming server rendering with Suspense boundaries. The pitfall is placing Suspense boundaries at the wrong level. Too high and the entire page waits. Too low and you get a flash of loading state for trivially fast data.
The right boundaries are around chunks of the UI that have meaningfully different data fetch times. Headers and navigation rendering immediately. Slow data fetches happening below their own Suspense boundary so they do not block the rest of the page.
This is more art than science and requires actual measurement of the data fetch times rather than guessing.
Pitfall six: middleware as a hammer
The middleware mechanism is powerful. Authentication checks, redirects, header rewrites, geo-based routing. The pitfall is putting too much logic into middleware because it works.
Middleware runs on every request. Heavy logic in middleware slows the entire site. The right scope for middleware is fast, simple, request-level concerns. Anything more complex belongs in the route handlers or the server components.
What worked for me
A reading list approach. Pick one App Router pattern. Build a small project that uses it. Read the relevant documentation section thoroughly. Move on to the next pattern.
The shortcut approach of building a large application and learning the patterns as you go produces frustration. The App Router is sufficiently different from the Pages Router that the learning has to happen deliberately.
What I would tell someone starting now
Read the official docs in order. The Next.js team has invested significant effort in the docs and they are good. Skipping them and learning from tutorials produces gaps that cost time later.
Build one production-style application before declaring yourself comfortable with the App Router. The patterns become natural with use, but the initial use has to happen on real work to make them stick.