Next.js App Router: 7 Mistakes I Made So You Do Not Have To
When Next.js 13 introduced the App Router, I jumped in immediately. Three production applications later, I have made every mistake worth making. Some cost me days of debugging, others caused production outages. All of them taught me something valuable.
Here are the seven biggest mistakes I made and how to avoid them.
Mistake 1: Making Everything a Client Component
My first instinct was to add "use client" to every component that used any interactivity — even a simple onClick handler on a button. This defeated the entire purpose of server components.
The correct mental model: start with everything as a server component. Only add "use client" when you need browser APIs (useState, useEffect, event handlers, window/document access). When you do need interactivity, push the "use client" boundary as deep as possible.
For example, a product page that displays data and has an "Add to Cart" button does not need "use client" on the entire page. The page fetches and renders server-side. Only the AddToCartButton component needs "use client" because it uses onClick and potentially useState for loading state.
On AIDToolStack, our tool listing pages are entirely server-rendered. The tool cards, categories, filters — all server components. Only the interactive elements (search input, filter dropdowns, bookmark buttons) are client components imported into the server-rendered page. This means the initial HTML is complete, fast, and SEO-friendly.
Mistake 2: Not Understanding the Caching Layers
Next.js App Router has four caching layers, and I did not understand any of them initially. This caused stale data in production that took hours to diagnose.
The Request Memoization cache deduplicates identical fetch calls within a single request. If three components fetch the same URL, only one network request is made. This is great and requires no action.
The Data Cache persists fetch results across requests. By default, fetch() in a server component caches indefinitely unless you opt out with cache: "no-store" or revalidate options. This caught me hard — I was fetching user-specific data that was being cached and served to other users. Add cache: "no-store" to any fetch that returns user-specific or frequently-changing data.
The Full Route Cache caches the rendered HTML of static routes at build time. Dynamic routes (those using cookies(), headers(), or searchParams) are not cached. If your page is unexpectedly static, check whether Next.js is treating it as cacheable.
The Router Cache on the client caches previously visited routes for 30 seconds (dynamic) or 5 minutes (static). This means navigating back to a page shows stale data. Use router.refresh() after mutations that should update the displayed data.
My rule now: explicitly set caching behavior on every fetch call. Never rely on defaults. Either cache: "no-store" for dynamic data or next: { revalidate: 300 } for data that changes infrequently.
Mistake 3: Fetching Data in the Wrong Place
In the Pages Router, data fetching was centralized in getServerSideProps or getStaticProps. In the App Router, you fetch data directly in the component that needs it. This is liberating but also dangerous.
My mistake: fetching the same data in multiple components instead of fetching once in a parent and passing down. While Request Memoization deduplicates the network calls, you still pay the cost of processing the response multiple times and you scatter data fetching logic across the component tree.
The better pattern: fetch data at the highest level that makes sense (usually the page.tsx or a layout.tsx), and pass it down as props. This makes data dependencies explicit and debugging straightforward. Use Suspense boundaries to show loading states for components that fetch their own data.
Mistake 4: Ignoring Loading and Error States
The App Router gives you loading.tsx and error.tsx files that automatically create Suspense and Error boundaries for each route segment. I initially skipped these because my pages loaded fast in development.
In production, on slow connections, users saw blank white screens for 2-3 seconds because I had no loading.tsx. When a database query failed, they saw the ugly default Next.js error page instead of a helpful message.
Every route that fetches data should have a loading.tsx with a meaningful skeleton UI. Every route should have an error.tsx that explains what went wrong and offers a retry button. These take 5 minutes to create and dramatically improve the experience for users on slow connections or during service disruptions.
Mistake 5: Overusing Route Handlers When Server Actions Work
I built a full REST API with route handlers (app/api/...) for form submissions and data mutations. Then I discovered server actions could handle 80% of those cases with less code, automatic revalidation, and progressive enhancement.
Server actions are functions that run on the server and can be called directly from client components. For form submissions, likes, bookmarks, and simple CRUD operations, they are simpler than building a route handler, calling it with fetch, handling the response, and manually revalidating.
I now use route handlers only for: third-party webhooks, API endpoints consumed by external services, and operations that need streaming responses. Everything else uses server actions.
Mistake 6: Not Using Parallel and Intercepting Routes
I built a photo gallery where clicking a photo opened a new page with the full image. The transition was jarring — full page navigation, loss of scroll position, slow back navigation. Users hated it.
The fix was intercepting routes. When a user clicks a photo, the route is intercepted and shown in a modal overlay. The URL changes (shareable, bookmarkable), but the page does not navigate. Clicking back closes the modal. If you share the URL directly, it loads the full photo page. Best of both worlds.
We use this pattern on AIDToolStack for tool previews. Clicking "Quick View" on a tool card opens a modal with the tool details. The URL changes to /tools/[slug], but you stay on the listing page. Direct navigation to /tools/[slug] loads the full page. Users love it because they can quickly scan multiple tools without losing their place.
Mistake 7: Fighting the Framework Instead of Understanding It
This is the meta-mistake that caused all the others. I approached the App Router with Pages Router mental models and fought every difference instead of understanding the reasoning.
Server components are not a limitation — they are the default because most UI does not need interactivity. Caching is aggressive by default because most data changes infrequently. Streaming is built-in because it provides a better user experience than waiting for everything.
Once I stopped fighting and started understanding, everything clicked. The App Router is opinionated, but the opinions are well-reasoned. Learn the mental model, work with the framework, and your applications will be faster, simpler, and more maintainable.
The Takeaway
The App Router is a significant paradigm shift from the Pages Router. Expect to make mistakes. The key is making them quickly, on small projects, before you build something large on incorrect assumptions. Build a small project, deploy it, test with real users, and internalize the mental model before migrating your production application.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.