Writing10 Common Next.js App Router Mistakes and How to Fix Them — Clixo
6 min readnext.js, app-router, common-mistakes, best-practices

10 Common Next.js App Router Mistakes and How to Fix Them

The most common Next.js App Router mistakes teams make — from misusing use client to broken caching assumptions — and the practical fixes for each.

Moving a team from the Pages Router to the App Router is not a rename exercise. The mental model is different enough that experienced Next.js developers still make the same set of mistakes in their first few App Router projects. Most of these mistakes do not throw obvious errors — they just silently degrade performance, bloat the bundle, or break caching in ways that are hard to diagnose later.

Here are the ten mistakes that appear most often, and the direct fix for each.

1. Putting use client Too High in the Tree

This is the most common mistake, and it compounds every other problem on this list. When use client appears on a page or a top-level layout, every component in that subtree becomes a client component. You lose server-side data access, bloat the JS bundle, and break any child component that uses server-only imports.

Fix: Move use client to the smallest possible leaf component that actually needs browser APIs or state. Most pages should be server components that import a handful of small client components for specific interactions.

2. Fetching Data in a useEffect When a Server Component Could Do It

Many developers carry a Pages Router habit into the App Router: fetch data client-side in useEffect because that's what they know. In the App Router, server components are async by default. They can await database queries or API calls directly in the component body.

Fix: Make your page or layout component async and fetch directly. Remove the loading state boilerplate. Wrap slow fetches in Suspense if you need to stream them in after initial HTML.

3. Ignoring the Data Cache

The App Router has a layered caching system. By default, fetch calls in server components are cached. Teams either don't know this and get stale data, or they disable caching everywhere with cache: 'no-store' and lose all the performance benefits.

Fix: Understand what each layer caches. Use revalidate with a time interval for content that changes periodically. Use revalidatePath or revalidateTag from a Server Action when you want on-demand invalidation after a mutation.

useRouter().push() works, but it is a client-side API that requires a client component boundary. Most internal navigation does not need that. A plain anchor tag wrapped in Next.js's Link component is lighter, works without JavaScript hydration, and participates in prefetching automatically.

Fix: Use Link from next/link for any navigation that does not depend on runtime logic. Reserve useRouter for programmatic redirects after form submissions or auth checks.

5. Not Using Route Groups to Organize Layouts

Teams often create deeply nested folder structures to share a layout across a subset of routes, which either causes layout nesting they don't want or forces them to repeat layout code.

Fix: Use route groups — folders wrapped in parentheses like (marketing) or (dashboard) — to share layouts without affecting the URL. A (dashboard) route group can have its own layout.tsx with sidebar navigation that does not appear on the marketing pages.

6. Running Middleware on Routes That Don't Need It

Next.js Middleware runs on every matched request, including static assets if the matcher is too broad. A matcher like '/' or '/:path*' with no exclusions will run your auth check on every font, image, and CSS file.

Fix: Write precise matchers. Exclude static files and API routes that handle their own auth. Profile the matcher before deploying to production.

7. Treating Server Actions as REST Endpoints

Server Actions are useful for form mutations — they run on the server and can be called directly from a form's action attribute without JavaScript. Some teams try to use them as a general-purpose data-fetching layer, calling them from useEffect or wrapping them in complex client state machines.

Fix: Use Server Actions for mutations triggered by user interaction. Use server component data fetching for reads. If you need a true API, build a Route Handler.

8. Not Configuring generateStaticParams for Dynamic Routes

Dynamic routes like [slug] are rendered dynamically (server-side on every request) unless you tell Next.js which slugs to pre-render at build time. For content-heavy sites, this means every page load hits the server and the database.

Fix: Export generateStaticParams from the page file to pre-render known slugs at build time. Combine with revalidate for ISR if the content changes without a redeploy.

9. Leaking Secrets Through Props Passed to Client Components

Server components can access secret environment variables. If those secrets (or data derived from them) are passed as props into a client component, they get serialized into the client-side React tree and become visible in the browser.

Fix: Never pass raw secret values as props. Pass only the data the client component needs to render. Validate on the server that the client is only receiving safe, sanitized output.

10. Skipping Error Boundaries and Loading UI

The App Router provides error.tsx and loading.tsx file conventions for handling errors and loading states at any level of the route hierarchy. Teams often skip these in the early stages and end up with blank screens or unhandled promise rejections in production.

Fix: Add loading.tsx with a skeleton or spinner at each route segment where data fetching happens. Add error.tsx to catch rendering errors and show a recovery UI. Both files activate automatically — no extra wiring required.


The App Router rewards teams who understand its model. The caching system, server components, and file conventions all work together — but only if you lean into them rather than working around them.

If you're building a Next.js application and want a team that ships these patterns correctly the first time, start a conversation with Clixo.