Claude Codeなら数秒でNext.jsのページを作れる。問題は、そのページが2023年のパターンで書かれているかもしれないことだ。
AIコーディングツールは、Pages Routerのコードや古いReactパターンを大量に含むデータセットで学習している。Claude CodeにNext.js 16の機能を頼むと、 見た目は正しいがApp Routerの規約に違反するコード を生成することがある。
この記事では、Claude CodeがやりがちなApp Routerの7大ミス、CLAUDE.mdで防ぐ設定方法、そしてNext.js 16内蔵のMCPサーバーによるリアルタイムエラーフィードバックの仕組みを解説する。
なぜClaude CodeはApp Routerを間違えるのか
根本的な原因はシンプルだ。 Claudeの学習データにはApp RouterよりPages Routerのコードが圧倒的に多い。 Pages Routerは7年間(2016〜2023)デフォルトだった。App Routerが安定版になったのはNext.js 13.4(2023年5月)——わずか3年前だ。
つまり、Claude Codeの「デフォルトの癖」は以下のようなパターンに傾く:
getServerSideProps(async Server Componentsではなく)next/routerのuseRouter(next/navigationではなく)- すべてをRoute Handlerで処理(Server Actionsではなく)
useEffectでデータ取得(Server Componentの直接アクセスではなく)
CLAUDE.mdは有効だが、それだけでは不十分だ。セッション開始時はルールに従っていても、コンテキストが埋まるにつれて古いパターンに戻ることがある。
本当の解決策は3層の防御だ:
- CLAUDE.md ——セッション開始時にClaudeが読む明示的なルール
- AGENTS.md ——
node_modules内のNext.jsドキュメントを参照させ、バージョンに正確な情報を提供 - next-devtools-mcp ——ビルドエラーや型エラーをリアルタイムでClaudeにフィードバック
AI生成コードで頻出するNext.jsの7大ミス
Vercelの公式ブログ、コミュニティの報告、そして3ヶ月間のClaude Code × Next.js実開発から集めた。
1. Server ComponentからRoute Handlerを呼ぶ
// NG — 不要なネットワークラウンドトリップ
// 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} />;
}
// OK — 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 Componentはサーバー上で実行される。データベース、ファイルシステム、内部APIに直接アクセスできる。自分自身のRoute Handlerを呼ぶのは、自分にHTTPリクエストを投げる無駄な処理だ。
2. "use client" の乱用
// NG — ページ全体がClient Componentになる
"use client";
export default function DashboardPage() {
// ここにあるものすべてがブラウザに送られる
}
// OK — インタラクティブな末端だけをClient Componentにする
// app/dashboard/page.tsx (Server Component — ディレクティブなし)
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" は最小のインタラクティブな末端コンポーネントにだけ付ける。Claude Codeはインタラクティブ要素が少しでもあると、ページレベルのファイルに付けがちだ。
3. Suspenseバウンダリの配置ミス
// NG — Suspenseが間違ったコンポーネントをラップしている
export default function Page() {
return <AsyncDataComponent />;
}
async function AsyncDataComponent() {
const data = await fetchData();
return (
<Suspense fallback={<Loading />}>
<DataView data={data} />
</Suspense>
);
}
// OK — Suspenseは非同期コンポーネントを外側からラップする
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<AsyncDataComponent />
</Suspense>
);
}
async function AsyncDataComponent() {
const data = await fetchData();
return <DataView data={data} />;
}
Suspenseは非同期処理を トリガーする コンポーネントを、親レベルからラップしなければならない。非同期コンポーネントの内側に置いても意味がない。
4. Client Componentでのmetadataエクスポート
// NG — Client Componentではmetadataが無視される
"use client";
export const metadata = { title: "Dashboard" };
// OK — metadataはServer Component(page.tsxまたはlayout.tsx)に書く
// app/dashboard/page.tsx ("use client" なし)
export const metadata = { title: "Dashboard" };
metadata エクスポートはServer Componentでのみ機能する。Claudeが "use client" をページファイルに追加すると、metadataはエラーメッセージなしで静かに動かなくなる。
5. Server Actionsでrevalidationが漏れる
// NG — ミューテーション後にUIが更新されない
"use server";
export async function createPost(formData: FormData) {
await db.post.create({ data: { title: formData.get("title") as string } });
// UIは古いデータを表示し続ける
}
// OK — 影響するパスを再検証する
"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. try/catch内でredirect()を使う
// NG — redirectは内部でthrowするので、try/catchに捕まる
"use server";
export async function loginAction(formData: FormData) {
try {
await authenticate(formData);
redirect("/dashboard"); // NEXT_REDIRECTがthrowされ、下のcatchに捕まる
} catch (error) {
return { error: "Login failed" }; // リダイレクトが発生しない
}
}
// OK — redirectを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() は内部で NEXT_REDIRECT という特殊なエラーをthrowする仕組みだ。try/catch内にあると、リダイレクトが飲み込まれてしまう。
7. Request APIへの同期アクセス(Next.js 16)
// NG — Next.js 16では同期アクセスが削除された
import { cookies } from "next/headers";
export default function Page() {
const cookieStore = cookies(); // Next.js 16ではビルドエラー
}
// OK — 非同期アクセスが必須
import { cookies } from "next/headers";
export default async function Page() {
const cookieStore = await cookies();
}
Next.js 16では cookies()、headers()、params、searchParams への同期アクセスが完全に削除された。Claude Codeは古い学習データから同期バージョンを頻繁に生成する。
Next.jsプロジェクト用CLAUDE.md設定
上記のミスを防ぐ実用的なCLAUDE.mdテンプレート。
# 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とバンドルドキュメント
Next.js 16は node_modules/next/dist/docs/ 内にドキュメントを同梱している。自動生成される AGENTS.md は、AIツールにコード生成前にこのドキュメントを参照するよう指示する。学習データの古さに関係なく、インストール済みバージョンに正確な情報を提供できる。
「古いパターン」ミスを防ぐ最も効果的な方法だ。ClaudeのNext.js APIの記憶に頼るのではなく、インストール済みバージョンの実際のドキュメントを読ませる。
next-devtools-mcp — Next.js 16内蔵のAIブリッジ
Next.js 16には /_next/mcp エンドポイントが内蔵されており、開発サーバーの状態をAIツールに公開する。next-devtools-mcp パッケージがClaude Codeをこのエンドポイントに接続する。
セットアップ
プロジェクトルートの .mcp.json に追加する:
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
開発サーバー(npm run dev)を起動すれば、Claude Codeが自動的に接続する。
提供されるツール
| ツール | 機能 |
|---|---|
get_errors | ビルドエラー、ランタイムエラー、型エラーをリアルタイムで返す |
get_logs | 開発サーバーのコンソール出力にアクセス |
get_page_metadata | ルート構造、コンポーネントタイプ、ページごとのレンダリングモード |
get_project_metadata | プロジェクト設定、Next.jsバージョン、開発サーバーURL |
get_server_action_by_id | 特定のServer Actionのソースファイルを特定 |
なぜ重要なのか
MCPがなければ、フィードバックループは「Claude がコードを書く → あなたがビルドエラーに気づく → エラーを貼り付ける → Claudeが修正する」だ。next-devtools-mcpがあれば、Claudeがエラーを 即座に 検知し、あなたが気づく前に自己修正できる。
上記の7大ミスに対してとくに効果的だ。Suspenseの配置ミスは目に見えるエラーを出さないこともあるが、get_page_metadata で予期しないレンダリング動作として表面化する。
Claude Code × Next.jsの実践ワークフロー
日常的なNext.js開発でClaude Codeを使うワークフロー。
1. セッションのセットアップ
# バックグラウンドで開発サーバーを起動
npm run dev
# Claude Codeを起動
claude
Claude Codeがnext-devtools-mcpに自動接続する。/context を実行してMCPが読み込まれているか確認する。
2. 機能開発サイクル
- 欲しい機能を説明する
- Claudeがコードを生成——CLAUDE.mdが7大ミスを防止
- next-devtools-mcpがエラーをリアルタイムでフィードバック
- Claudeがエラーを貼り付けなくても自己修正
- 生成されたコードのロジックの正しさをレビュー
3. コミット前の検証
PostToolUse Hookでファイル編集後に型チェックを自動実行する:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx tsc --noEmit --pretty 2>&1 | head -20"
}
]
}
]
}
}
4. コンテキスト管理
- 1つの機能に長時間取り組むとき →
/compact - 別の機能に切り替えるとき →
/clear - 複数セッションにまたがる作業 → session-handoff.md
コンテキスト管理の詳細は「Claude Codeが忘れる原因と対策 完全ガイド」を参照。
まとめ
Claude CodeはNext.js開発に非常に強力だ——適切なガードレールを設定すれば。
| レイヤー | 役割 | セットアップ |
|---|---|---|
| CLAUDE.md | App Routerの7大ミスを防止 | 5分 |
| AGENTS.md | バージョン正確なNext.jsドキュメントでClaudeを根拠づける | create-next-appで自動 |
| next-devtools-mcp | リアルタイムエラーフィードバックループ | 2分 |
| Hooks | 編集後に型チェックを自動実行 | 5分 |
パターンはシンプルだ。CLAUDE.mdで制約し、バンドルドキュメントで根拠を与え、MCPで検証する。この3層を正しく設定すれば、Claude Codeは2026年のコードベースに2023年のパターンを生成しなくなる。
関連記事: