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.
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:
getServerSidePropsinstead of async Server ComponentsuseRouterfromnext/routerinstead ofnext/navigation- Route Handlers for everything instead of Server Actions for mutations
useEffectfor 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:
- CLAUDE.md — explicit rules Claude reads at session start
- AGENTS.md — points Claude to bundled Next.js docs in
node_modulesfor version-accurate reference - 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
// 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} />;
}
// 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"
// WRONG — entire page becomes a Client Component
"use client";
export default function DashboardPage() {
// Everything here ships to the browser
}
// 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
// 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>
);
}
// 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
// WRONG — metadata export is ignored in Client Components
"use client";
export const metadata = { title: "Dashboard" };
// 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
// 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
}
// 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
// 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
}
}
// 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)
// 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
}
// 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.
# 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:
{
"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
| Tool | What it does |
|---|---|
get_errors | Returns build errors, runtime errors, and type errors in real time |
get_logs | Access to dev server console output |
get_page_metadata | Route structure, component types, rendering mode per page |
get_project_metadata | Project config, Next.js version, dev server URL |
get_server_action_by_id | Locate 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
# 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
- Describe the feature you want
- Claude generates code — CLAUDE.md prevents the 7 common mistakes
- next-devtools-mcp feeds back any errors in real time
- Claude self-corrects without you needing to paste errors
- Review the generated code for logic correctness
3. Pre-commit validation
Use a PostToolUse Hook to auto-run type checking after file edits:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx tsc --noEmit --pretty 2>&1 | head -20"
}
]
}
]
}
}
4. Context management
/compactwhen working on one feature for a long time/clearwhen 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.
| Layer | What it does | Setup effort |
|---|---|---|
| CLAUDE.md | Prevents the 7 common App Router mistakes | 5 minutes |
| AGENTS.md | Grounds Claude in version-accurate Next.js docs | Automatic with create-next-app |
| next-devtools-mcp | Real-time error feedback loop | 2 minutes |
| Hooks | Auto-run type checks after edits | 5 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: