32blogby StudioMitsu

Claude Code × Next.js: Stop AI-Generated App Router Mistakes

The 7 most common App Router mistakes Claude Code makes, how to prevent them with CLAUDE.md, and how to use next-devtools-mcp for a real-time feedback loop.

10 min read
On this page

Claude Code can scaffold a Next.js page in seconds. The problem is that the page might use patterns from 2023.

AI coding tools are trained on massive datasets that include years of Pages Router code, pre-App Router patterns, and outdated React practices. When you ask Claude Code to build a Next.js 16 feature, it often generates code that looks correct but violates App Router conventions in subtle, hard-to-catch ways.

This article covers the 7 most common App Router mistakes Claude Code makes, how to prevent them with the right CLAUDE.md configuration, and how to use Next.js 16's built-in MCP server for a real-time error feedback loop.

CLAUDE.mdApp Router rulesConstrainAGENTS.mdBundled docs referenceGroundnext-devtools-mcpReal-time error feedValidateCorrect CodeNo silent mistakes

Why Claude Code Gets App Router Wrong

The core issue is simple: Claude's training data contains far more Pages Router code than App Router code. The Pages Router was the default for 7 years (2016–2023). The App Router became stable in Next.js 13.4 (May 2023) — barely 3 years ago.

This means Claude Code's "default instinct" leans toward patterns like:

  • getServerSideProps instead of async Server Components
  • useRouter from next/router instead of next/navigation
  • Route Handlers for everything instead of Server Actions for mutations
  • useEffect for data fetching instead of Server Component direct access

CLAUDE.md helps, but it's not enough on its own. Claude may follow your rules at session start and gradually drift back to old patterns as context fills up.

The real solution is a three-layer defense:

  1. CLAUDE.md — explicit rules Claude reads at session start
  2. AGENTS.md — points Claude to bundled Next.js docs in node_modules for version-accurate reference
  3. next-devtools-mcp — feeds build errors and type errors back to Claude in real time

The 7 Most Common AI-Generated Next.js Mistakes

These come from Vercel's official blog, community reports, and three months of daily Claude Code + Next.js development.

1. Calling Route Handlers from Server Components

typescript
// WRONG — unnecessary network roundtrip
// app/users/page.tsx (Server Component)
export default async function UsersPage() {
  const res = await fetch("http://localhost:3000/api/users");
  const users = await res.json();
  return <UserList users={users} />;
}
typescript
// CORRECT — direct database access in Server Component
// app/users/page.tsx (Server Component)
import { db } from "@/lib/db";

export default async function UsersPage() {
  const users = await db.user.findMany();
  return <UserList users={users} />;
}

Server Components run on the server. They can access databases, file systems, and internal APIs directly. Calling your own Route Handler creates an unnecessary HTTP request to yourself.

2. Overusing "use client"

typescript
// WRONG — entire page becomes a Client Component
"use client";

export default function DashboardPage() {
  // Everything here ships to the browser
}
typescript
// CORRECT — only the interactive leaf is a Client Component
// app/dashboard/page.tsx (Server Component — no directive)
import { InteractiveChart } from "./interactive-chart";

export default async function DashboardPage() {
  const data = await fetchDashboardData();
  return (
    <div>
      <h1>Dashboard</h1>
      <StaticSummary data={data} />
      <InteractiveChart data={data} />
    </div>
  );
}

"use client" should only appear on the smallest interactive leaf components. Claude tends to add it to page-level files at the first sign of interactivity.

3. Wrong Suspense Boundary Placement

tsx
// WRONG — Suspense wrapping the wrong component
export default function Page() {
  return <AsyncDataComponent />;
}

async function AsyncDataComponent() {
  const data = await fetchData();
  return (
    <Suspense fallback={<Loading />}>
      <DataView data={data} />
    </Suspense>
  );
}
tsx
// CORRECT — Suspense wraps the async component from OUTSIDE
export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <AsyncDataComponent />
    </Suspense>
  );
}

async function AsyncDataComponent() {
  const data = await fetchData();
  return <DataView data={data} />;
}

Suspense must wrap the component that triggers the async operation, from the parent level. Placing it inside the async component defeats its purpose.

4. Metadata in Client Components

typescript
// WRONG — metadata export is ignored in Client Components
"use client";

export const metadata = { title: "Dashboard" };
typescript
// CORRECT — metadata must be in a Server Component (page.tsx or layout.tsx)
// app/dashboard/page.tsx (no "use client")
export const metadata = { title: "Dashboard" };

The metadata export only works in Server Components. If Claude adds "use client" to a page file, metadata silently stops working with no error message.

5. Missing revalidation in Server Actions

typescript
// WRONG — UI doesn't update after mutation
"use server";

export async function createPost(formData: FormData) {
  await db.post.create({ data: { title: formData.get("title") as string } });
  // UI shows stale data
}
typescript
// CORRECT — revalidate the affected path
"use server";

import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  await db.post.create({ data: { title: formData.get("title") as string } });
  revalidatePath("/posts");
}

6. Using redirect() inside try/catch

typescript
// WRONG — redirect throws internally, gets caught by try/catch
"use server";

export async function loginAction(formData: FormData) {
  try {
    await authenticate(formData);
    redirect("/dashboard"); // This throws NEXT_REDIRECT, caught below
  } catch (error) {
    return { error: "Login failed" }; // Redirect never happens
  }
}
typescript
// CORRECT — redirect outside try/catch
"use server";

export async function loginAction(formData: FormData) {
  let success = false;
  try {
    await authenticate(formData);
    success = true;
  } catch (error) {
    return { error: "Login failed" };
  }
  if (success) redirect("/dashboard");
}

redirect() works by throwing a special NEXT_REDIRECT error. If it's inside try/catch, the redirect gets swallowed.

7. Synchronous access to Request APIs (Next.js 16)

typescript
// WRONG — synchronous access removed in Next.js 16
import { cookies } from "next/headers";

export default function Page() {
  const cookieStore = cookies(); // Build error in Next.js 16
}
typescript
// CORRECT — async access required
import { cookies } from "next/headers";

export default async function Page() {
  const cookieStore = await cookies();
}

Next.js 16 fully removed synchronous access to cookies(), headers(), params, and searchParams. Claude frequently generates the synchronous version from older training data.


CLAUDE.md Configuration for Next.js Projects

Here's a practical CLAUDE.md that prevents the mistakes above.

markdown
# Project: Next.js 16 App Router

## Tech Stack
- Next.js 16.1 + TypeScript strict + Tailwind CSS v4
- App Router only. No Pages Router patterns
- React Server Components by default

## Critical Rules
- NEVER add "use client" to page.tsx or layout.tsx unless the entire page must be interactive
- NEVER call Route Handlers from Server Components — access data directly
- ALWAYS use async/await for cookies(), headers(), params, searchParams
- ALWAYS place Suspense boundaries OUTSIDE the async component
- ALWAYS call revalidatePath() or revalidateTag() in Server Actions after mutations
- NEVER put redirect() inside try/catch blocks

## Data Fetching
- Server Components: direct database/API access (no fetch to own Route Handlers)
- Client Components: Server Actions for mutations, Route Handlers only for third-party webhooks
- Cache: use the "use cache" directive for explicit caching

## Reference
- Read AGENTS.md for bundled Next.js docs location
- When unsure about an API, check node_modules/next/dist/docs/ first

AGENTS.md and Bundled Docs

Next.js 16 ships documentation inside node_modules/next/dist/docs/. The auto-generated AGENTS.md tells AI tools to reference these docs before generating code, ensuring version-accurate guidance regardless of training data age.

This is the single most effective way to prevent "old pattern" mistakes. Instead of relying on Claude's memory of Next.js APIs, it reads the actual docs for the version you've installed.


next-devtools-mcp — Next.js 16's Built-In AI Bridge

Next.js 16 includes a /_next/mcp endpoint that exposes development server state to AI tools. The next-devtools-mcp package connects Claude Code to this endpoint.

Setup

Add to .mcp.json in your project root:

json
{
  "mcpServers": {
    "next-devtools": {
      "command": "npx",
      "args": ["-y", "next-devtools-mcp@latest"]
    }
  }
}

Then start your dev server (npm run dev) and Claude Code will automatically connect.

What it provides

ToolWhat it does
get_errorsReturns build errors, runtime errors, and type errors in real time
get_logsAccess to dev server console output
get_page_metadataRoute structure, component types, rendering mode per page
get_project_metadataProject config, Next.js version, dev server URL
get_server_action_by_idLocate the source file for a specific Server Action

Why this matters

Without MCP, the feedback loop is: Claude writes code → you notice a build error → you paste it back → Claude fixes it. With next-devtools-mcp, Claude sees the error immediately and can self-correct before you even notice.

This is especially powerful for the type of mistakes listed above. A wrong Suspense placement might not throw a visible error but will show up in get_page_metadata as unexpected rendering behavior.


A Real-World Claude Code × Next.js Workflow

Here's the workflow that's worked for daily Next.js development with Claude Code.

1. Session setup

bash
# Start dev server in background
npm run dev

# Start Claude Code
claude

Claude Code connects to next-devtools-mcp automatically. Run /context to verify MCP is loaded.

2. Feature development cycle

  1. Describe the feature you want
  2. Claude generates code — CLAUDE.md prevents the 7 common mistakes
  3. next-devtools-mcp feeds back any errors in real time
  4. Claude self-corrects without you needing to paste errors
  5. Review the generated code for logic correctness

3. Pre-commit validation

Use a PostToolUse Hook to auto-run type checking after file edits:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx tsc --noEmit --pretty 2>&1 | head -20"
          }
        ]
      }
    ]
  }
}

4. Context management

  • /compact when working on one feature for a long time
  • /clear when switching to a different feature
  • Session-handoff.md for multi-session work

For a deep dive on context management strategies, see "Why Claude Code Forgets and How to Fix It."


Wrapping Up

Claude Code is remarkably capable at Next.js development — once you give it the right guardrails.

LayerWhat it doesSetup effort
CLAUDE.mdPrevents the 7 common App Router mistakes5 minutes
AGENTS.mdGrounds Claude in version-accurate Next.js docsAutomatic with create-next-app
next-devtools-mcpReal-time error feedback loop2 minutes
HooksAuto-run type checks after edits5 minutes

The pattern is simple: constrain with CLAUDE.md, ground with bundled docs, validate with MCP. Get these three layers right and Claude Code stops generating 2023 patterns for your 2026 codebase.

Related articles: