Next.js App Router Gotchas: Common Mistakes Developers Make (and How to Avoid Them)

Next.js App Router gotchas are common mistakes developers encounter when building applications with Next.js’s App Router, especially around server components, layouts, routing behavior, and data fetching. These issues are not bugs in Next.js—they result from how the framework layers performance optimizations like streaming, caching, and server rendering on top of standard browser behavior.

This guide explains the most common Next.js App Router pitfalls in clear, practical terms, showing why they happen and how to avoid them. Understanding these gotchas early helps prevent unexpected rendering behavior, broken navigation, and hours of unnecessary debugging in production apps.

Input type=”number” returns a string

A common pitfall is assuming that a number input returns a number. Even with type="number", the value is still text.

<input type="number" onChange={(e) => setAmount(e.target.value)} />

If you use the value without converting it, calculations may behave incorrectly.

setAmount(Number(e.target.value));

Server Components Cannot Access Browser APIs

Next.js Server Components do not run in the browser and cannot access browser-only APIs. Objects such as window, document, and localStorage are unavailable by design.

Examples of unsupported APIs in Server Components:

  • window

  • localStorage

  • navigator

If a component requires browser access or user interaction, it must be explicitly marked as a Client Component.

"use client";

useEffect Only Runs in the Browser

In Next.js, useEffect never runs on the server. It executes only in the browser after the page has loaded and hydrated.

Because of this:

  • Data loaded inside useEffect appears after the initial render

  • Server-rendered HTML may differ from client-rendered content

  • Developers may incorrectly assume data is available earlier than it is

This behavior is expected and should be accounted for when deciding where data fetching and side effects belong.

Fetch Caching Can Make Data Updates Look Broken

Next.js automatically caches fetch requests by default in the App Router. This optimization can cause API updates to appear stale or ignored if caching behavior is not explicitly configured.

For dynamic or frequently updated data, caching should be disabled:

fetch("/api/posts", { cache: "no-store" });

Without proper cache control, applications may display outdated data even when backend updates are successful.

Route Parameters Are Always Strings

All route parameters in Next.js are provided as strings, even if they appear numeric. This frequently causes bugs when parameters are used in calculations or database queries.

const id = Number(params.id);

The action Attribute Behaves Differently in the App Router

In the Next.js App Router, a form’s action attribute can reference a server function instead of a URL. This enables forms to invoke server-side logic directly without client-side JavaScript.

This behavior differs from standard HTML forms and is a common source of confusion for developers new to the App Router.

Using redirect outside of try/catch blocks

Next.js redirect works by throwing a special response that tells the router to navigate. If you call redirect inside a try/catch block, the error it throws will be caught, preventing the redirect from working.

To handle this correctly, call redirect after the try/catch block. This ensures the redirect only happens if the code in try succeeds.

let data;
try {
    data = await fetchData();
} catch (err) {
    console.error(err);
}

// Only redirect after try/catch
if (!data) {
    redirect("/error");
}

Public Environment Variables Are Exposed to Users

Any environment variable prefixed with NEXT_PUBLIC_ is exposed to the browser. These values are included in client-side JavaScript and visible to anyone using developer tools.

Sensitive data such as:

  • API secrets
  • Private tokens

  • Internal URLs

should never use the NEXT_PUBLIC_ prefix.

Final Thoughts on Next.js Gotchas

Most Next.js App Router issues stem from the same underlying concepts:

  • Browsers treat many values as strings by default

  • Server and browser environments have fundamentally different capabilities

  • Next.js adds framework-level optimizations like caching and streaming

When behavior feels confusing, the most useful question to ask is:

“Is this standard browser behavior, or a Next.js App Router optimization?”

That distinction explains the majority of Next.js gotchas—and saves hours of debugging.

Ready to Start Your Project?