32blogby Studio Mitsu

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.

by omitsu13 min read
On this page

The fix for AI-generated App Router mistakes is a three-layer defense: CLAUDE.md rules to prevent the 7 most common errors, AGENTS.md to ground Claude in version-accurate docs, and next-devtools-mcp for real-time error feedback.

When you combine Claude Code with Next.js 16 (App Router), certain mistakes come up again and again — "use client" on page files, Route Handler calls from Server Components, synchronous cookies() that worked in Next.js 15 but broke in 16. The three-layer setup described here drops those mistakes to near zero.

This article covers the 7 most common App Router mistakes, how to prevent them with CLAUDE.md, and how next-devtools-mcp creates 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 on 32blog.

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. In Next.js 15 these were deprecated with warnings; in 16 they throw build errors. Claude frequently generates the synchronous version from older training data.

This is a common pain point when migrating from Next.js 15 to 16. The codemod (npx @next/codemod@latest next-async-request-api .) handles most cases, but Claude keeps regenerating the synchronous pattern until you add the explicit rule to CLAUDE.md.


CLAUDE.md Configuration for Next.js Projects

Here's a practical CLAUDE.md that prevents the mistakes above. For a deep dive on CLAUDE.md design patterns beyond Next.js, see "Claude Code Context Management: CLAUDE.md Design Patterns."

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

Starting with Next.js 16.2, the full Next.js documentation is bundled as plain Markdown files at 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. For earlier versions, you can generate them with npx @next/codemod@latest agents-md.

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. Vercel's internal testing showed a 100% pass rate on Next.js evals with bundled docs, compared to 79% with on-demand retrieval.


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. If you're new to Claude Code's slash commands, check the "Claude Code Commands Cheatsheet."

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." If context window limits are an issue, "How to Fix Context Window Exceeded in Claude Code" covers specific solutions.


Frequently Asked Questions

Does Claude Code work with the Pages Router?

Yes, Claude Code works with both the Pages Router and App Router. The issues in this article are specifically about App Router conventions. If your project still uses the Pages Router, Claude Code's defaults actually align better since the training data contains more Pages Router patterns.

Do I need all three layers (CLAUDE.md, AGENTS.md, MCP)?

Each layer catches different problems. CLAUDE.md prevents known mistakes proactively. AGENTS.md ensures version accuracy. MCP catches runtime errors Claude can't predict. You can start with CLAUDE.md alone and add the others incrementally — but in my experience, the real productivity jump came when all three were active.

Does next-devtools-mcp work with other AI coding tools?

Yes. The /_next/mcp endpoint uses the open Model Context Protocol, so any MCP-compatible tool can connect. Cursor, Windsurf, and GitHub Copilot (with MCP support) can all use the same setup.

How much does next-devtools-mcp cost in tokens?

MCP tool calls do consume context tokens. In practice, get_errors returns compact JSON — typically 200–500 tokens per call. The token cost is far less than manually pasting build errors. Monitor usage with /context and disconnect MCP servers you don't need.

Will CLAUDE.md rules get ignored as the context fills up?

It can happen. CLAUDE.md is loaded at session start, and as context grows, earlier instructions carry less weight. That's exactly why the three-layer approach matters — even if CLAUDE.md rules drift, AGENTS.md and MCP provide independent guardrails. Use /compact regularly to keep context lean.

Can I use this setup with Next.js 15?

Most of the CLAUDE.md rules apply to Next.js 15 as well, except for mistake #7 (async Request APIs), which was only a deprecation warning in 15. The bundled docs in node_modules/next/dist/docs/ are a Next.js 16.2+ feature, but you can generate AGENTS.md manually with the Next.js codemod.

What's the difference between CLAUDE.md and AGENTS.md?

CLAUDE.md is Claude Code-specific — it contains project rules, conventions, and constraints that Claude reads at session start. AGENTS.md is tool-agnostic and tells any AI coding tool where to find reference documentation. In Next.js projects, AGENTS.md points to the bundled docs, while CLAUDE.md defines behavioral rules like "never use 'use client' on page files."


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: