𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
Claude Code × Next.js App Router開発ガイド【RSC・Server Actions完全対応】

Claude Code × Next.js App Router開発ガイド【RSC・Server Actions完全対応】

Claude CodeNext.jsApp RouterReact Server Componentsフルスタック開発
目次
⚡ 3秒でわかる!この記事のポイント
  • Claude CodeはNext.js App Routerの複雑なファイル規約(layout/page/loading/error)を理解し、正確なルーティング構造を自動生成する
  • React Server Components(RSC)とServer Actionsのコード生成では、クライアント/サーバー境界を適切に判断してくれる
  • Parallel Routes・Intercepting Routes・Route Groupsなど高度なパターンもプロンプト一つで実装可能

「Next.js App Routerに移行したいけど、ファイル構造やServer Componentsの使い分けが複雑すぎる…」 「RSCとClient Componentsの境界をどこに引けばいいか、毎回迷ってしまう…」

Next.js App Routerは強力なフレームワークですが、その複雑なファイル規約とサーバー/クライアントの境界管理は、多くのエンジニアにとって悩みの種です。Claude Codeを活用すれば、これらの複雑さを大幅に軽減し、App Routerの本来のパワーを引き出した開発が可能になります。

この記事では、Claude Codeを使ったNext.js App Router開発の実践テクニックを、初期セットアップから高度なルーティングパターンまで体系的に解説します。

この記事でわかること
  • Claude CodeでApp Routerプロジェクトを効率的にセットアップする方法
  • RSCとClient Componentsの境界を正しく設計するプロンプト術
  • Server Actions・Parallel Routes・Route Groupsの実装パターン
  • SES現場でのNext.js開発案件に活かせる実践テクニック

Next.js App Routerの複雑さとClaude Codeの相性

App Routerが難しい理由

Next.js App Router(v13以降)は、従来のPages Routerから大幅にアーキテクチャが変更されました。主な複雑さのポイントは以下の通りです。

  • ファイル規約の多さ: page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, template.tsx, default.tsx など、特殊ファイルが10種類以上
  • Server/Client境界: デフォルトがServer Componentになり、'use client'ディレクティブの配置が重要に
  • データフェッチの変遷: getServerSideProps廃止、async Server Componentsへの移行
  • キャッシュ戦略: 4層のキャッシュ(Request Memoization, Data Cache, Full Route Cache, Router Cache)の理解が必要
  • ルーティングの高度化: Parallel Routes、Intercepting Routes、Route Groupsなど新概念の追加

Claude Codeが解決する問題

Claude Codeは、これらの複雑さに対して以下の価値を提供します。

課題Claude Codeの解決策
ファイル規約の暗記適切なファイルを適切な場所に自動生成
Server/Client境界の判断コンポーネントの性質に応じて自動で'use client'を付与
データフェッチパターンasync RSC / Server Actions / Route Handlersを使い分け
キャッシュ戦略の設定revalidatedynamicの適切な値を提案
TypeScript型定義ルートパラメータやフォームデータの型を自動生成

CLAUDE.mdでNext.jsプロジェクトを設定する

推奨するCLAUDE.md設定

Claude Codeの動作をNext.jsプロジェクトに最適化するために、CLAUDE.mdに以下の設定を記述します。

# Next.js App Router プロジェクト

## アーキテクチャ
- Next.js 15 + App Router
- React 19 Server Components(デフォルト)
- TypeScript strict mode
- Tailwind CSS v4 for styling
- Drizzle ORM for database

## ファイル規約
- app/ ディレクトリ構造に従う
- Server Componentsがデフォルト。'use client'は必要な場合のみ
- コンポーネントは components/ に集約
- Server Actionsは app/_actions/ に配置
- 型定義は types/ ディレクトリ

## コーディングルール
- データフェッチはasync Server Componentsで行う
- フォーム送信はServer Actionsを使う
- クライアント状態が必要なコンポーネントのみ'use client'
- エラーハンドリングはerror.tsxで統一
- loading.tsxでSuspense境界を設定

## テスト
- Vitest + React Testing Library
- E2E: Playwright

この設定により、Claude Codeは生成するコードの品質を大幅に向上させます。CLAUDE.md活用ガイドも参考にしてください。

プロジェクト初期化プロンプト

Next.js 15 App Routerプロジェクトを作成して。
以下の構成にしてほしい:
- TypeScript strict mode
- Tailwind CSS v4
- src/app ディレクトリ構造
- 認証(NextAuth.js v5)
- DB: PostgreSQL + Drizzle ORM
- ダッシュボード用のRoute Group: (dashboard)
- 公開ページ用のRoute Group: (public)
- 共通レイアウト(ヘッダー・サイドバー・フッター)

Claude Codeはこのプロンプトから、以下のような構造を自動生成します。

src/app/
├── (dashboard)/
│   ├── layout.tsx          # ダッシュボード用レイアウト(サイドバー付き)
│   ├── dashboard/
│   │   └── page.tsx
│   ├── settings/
│   │   └── page.tsx
│   └── projects/
│       ├── page.tsx
│       └── [id]/
│           └── page.tsx
├── (public)/
│   ├── layout.tsx          # 公開ページ用レイアウト
│   ├── page.tsx            # トップページ
│   ├── about/
│   │   └── page.tsx
│   └── pricing/
│       └── page.tsx
├── api/
│   └── auth/
│       └── [...nextauth]/
│           └── route.ts
├── layout.tsx              # ルートレイアウト
├── not-found.tsx
└── global-error.tsx

React Server Components(RSC)の活用

Server/Client境界の設計プロンプト

RSCで最も重要なのは、どのコンポーネントをServer Componentにし、どれをClient Componentにするかの判断です。Claude Codeには以下のようなプロンプトで明確に指示できます。

ユーザープロフィールページを作成して。以下の要件で:

Server Components(データフェッチ担当):
- UserProfilePage: DBからユーザー情報を取得
- UserPosts: ユーザーの投稿一覧を取得
- UserStats: 統計情報を取得

Client Components(インタラクション担当):
- EditProfileForm: プロフィール編集フォーム(useState使用)
- FollowButton: フォローボタン(楽観的更新)
- PostFilter: 投稿のフィルタリングUI

Claude Codeが生成するServer Componentの例:

// app/users/[id]/page.tsx — Server Component(デフォルト)
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { UserStats } from './_components/user-stats';
import { UserPosts } from './_components/user-posts';
import { EditProfileForm } from './_components/edit-profile-form';

interface Props {
  params: Promise<{ id: string }>;
}

export default async function UserProfilePage({ params }: Props) {
  const { id } = await params;
  const user = await db.query.users.findFirst({
    where: eq(users.id, id),
    with: { profile: true },
  });

  if (!user) notFound();

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold">{user.name}</h1>

      {/* Server Component: データフェッチを含む */}
      <UserStats userId={user.id} />

      {/* Client Component: フォーム操作が必要 */}
      <EditProfileForm user={user} />

      {/* Server Component: 投稿一覧の取得 */}
      <UserPosts userId={user.id} />
    </div>
  );
}

export async function generateMetadata({ params }: Props) {
  const { id } = await params;
  const user = await db.query.users.findFirst({
    where: eq(users.id, id),
  });
  return {
    title: user ? `${user.name}のプロフィール` : 'ユーザーが見つかりません',
    description: user?.profile?.bio ?? '',
  };
}

対応するClient Componentの例:

// app/users/[id]/_components/edit-profile-form.tsx
'use client';

import { useState, useTransition } from 'react';
import { updateProfile } from '@/app/_actions/user';
import type { User } from '@/types';

export function EditProfileForm({ user }: { user: User }) {
  const [isPending, startTransition] = useTransition();
  const [optimisticName, setOptimisticName] = useState(user.name);

  async function handleSubmit(formData: FormData) {
    const newName = formData.get('name') as string;
    setOptimisticName(newName);

    startTransition(async () => {
      await updateProfile(user.id, formData);
    });
  }

  return (
    <form action={handleSubmit} className="space-y-4">
      <input
        name="name"
        defaultValue={optimisticName}
        className="border rounded px-3 py-2"
      />
      <button
        type="submit"
        disabled={isPending}
        className="bg-blue-600 text-white px-4 py-2 rounded"
      >
        {isPending ? '保存中...' : '保存'}
      </button>
    </form>
  );
}

RSCでよくあるエラーとClaude Codeの対処

Claude Codeは、RSCでよくあるエラーパターンを理解し、適切に回避してくれます。

エラー原因Claude Codeの対処
useState is not definedServer ComponentでuseStateを使用自動で'use client'を付与するか、設計を分離
Event handlers in Server ComponentsServer ComponentにonClick等を配置インタラクション部分をClient Componentに抽出
Async Client ComponentClient ComponentをasyncにしたデータフェッチをServer Componentに移動
Serialization errorPropsにDate/Map等を渡したシリアライズ可能な形式に変換

Claude Code × Next.js App Router開発の全体像

Server Actionsの実装パターン

フォーム送信の基本パターン

Server Actionsは、Next.js App Routerにおけるデータ変更の推奨パターンです。Claude Codeに以下のように指示すると、型安全なServer Actionsを生成してくれます。

お問い合わせフォームのServer Actionを作成して。
- Zod でバリデーション
- DB保存 + メール送信
- エラーハンドリング(バリデーションエラー / サーバーエラー)
- 楽観的更新対応
- revalidatePathで一覧ページを更新

生成されるServer Action:

// app/_actions/contact.ts
'use server';

import { z } from 'zod';
import { db } from '@/lib/db';
import { contacts } from '@/lib/db/schema';
import { revalidatePath } from 'next/cache';
import { sendEmail } from '@/lib/email';

const contactSchema = z.object({
  name: z.string().min(1, '名前は必須です').max(100),
  email: z.string().email('有効なメールアドレスを入力してください'),
  company: z.string().optional(),
  message: z.string().min(10, 'メッセージは10文字以上で入力してください').max(5000),
  category: z.enum(['general', 'support', 'partnership']),
});

export type ContactFormState = {
  success: boolean;
  errors?: Record<string, string[]>;
  message?: string;
};

export async function submitContact(
  prevState: ContactFormState,
  formData: FormData
): Promise<ContactFormState> {
  const rawData = Object.fromEntries(formData);

  const result = contactSchema.safeParse(rawData);
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    };
  }

  try {
    const [contact] = await db.insert(contacts).values({
      ...result.data,
      createdAt: new Date(),
    }).returning();

    await sendEmail({
      to: '[email protected]',
      subject: `お問い合わせ: ${result.data.category}`,
      body: `${result.data.name}様からのお問い合わせ\n\n${result.data.message}`,
    });

    revalidatePath('/admin/contacts');

    return {
      success: true,
      message: 'お問い合わせを受け付けました',
    };
  } catch (error) {
    console.error('Contact submission error:', error);
    return {
      success: false,
      message: 'サーバーエラーが発生しました。時間をおいて再度お試しください。',
    };
  }
}

useActionState と useOptimistic の活用

React 19 の useActionStateuseOptimistic を組み合わせたパターンも、Claude Codeは正確に生成します。

// components/contact-form.tsx
'use client';

import { useActionState, useOptimistic } from 'react';
import { submitContact, type ContactFormState } from '@/app/_actions/contact';

const initialState: ContactFormState = { success: false };

export function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitContact, initialState);

  return (
    <form action={formAction} className="space-y-6">
      {state.message && (
        <div className={`p-4 rounded ${state.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'}`}>
          {state.message}
        </div>
      )}

      <div>
        <label htmlFor="name" className="block text-sm font-medium">名前</label>
        <input id="name" name="name" required className="mt-1 block w-full rounded border px-3 py-2" />
        {state.errors?.name && (
          <p className="mt-1 text-sm text-red-600">{state.errors.name[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium">メールアドレス</label>
        <input id="email" name="email" type="email" required className="mt-1 block w-full rounded border px-3 py-2" />
        {state.errors?.email && (
          <p className="mt-1 text-sm text-red-600">{state.errors.email[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="category" className="block text-sm font-medium">カテゴリ</label>
        <select id="category" name="category" className="mt-1 block w-full rounded border px-3 py-2">
          <option value="general">一般的なお問い合わせ</option>
          <option value="support">技術サポート</option>
          <option value="partnership">パートナーシップ</option>
        </select>
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium">メッセージ</label>
        <textarea id="message" name="message" rows={5} required className="mt-1 block w-full rounded border px-3 py-2" />
        {state.errors?.message && (
          <p className="mt-1 text-sm text-red-600">{state.errors.message[0]}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={isPending}
        className="w-full bg-blue-600 text-white py-3 rounded font-semibold hover:bg-blue-700 disabled:opacity-50"
      >
        {isPending ? '送信中...' : '送信する'}
      </button>
    </form>
  );
}

Parallel RoutesとIntercepting Routes

Parallel Routesの実装

Parallel Routesは、同一レイアウト内で複数のページを同時にレンダリングする強力な機能です。ダッシュボード画面での活用例をClaude Codeに指示してみましょう。

ダッシュボードにParallel Routesを実装して。
左カラム: @analytics(グラフ表示)
右カラム: @notifications(通知一覧)
モーダル: @modal(詳細表示)
各スロットに独立したloading.tsxとerror.tsxを配置

生成されるファイル構造:

src/app/(dashboard)/dashboard/
├── layout.tsx              # Parallel Routesのスロットを受け取る
├── page.tsx                # メインコンテンツ
├── @analytics/
│   ├── page.tsx           # 分析グラフ
│   ├── loading.tsx        # 分析データ読み込み中
│   ├── error.tsx          # 分析エラー
│   └── default.tsx        # デフォルト表示
├── @notifications/
│   ├── page.tsx           # 通知一覧
│   ├── loading.tsx        # 通知読み込み中
│   ├── error.tsx          # 通知エラー
│   └── default.tsx        # デフォルト表示
└── @modal/
    ├── default.tsx        # モーダル非表示時
    └── (..)projects/[id]/
        └── page.tsx       # プロジェクト詳細モーダル

レイアウトファイル:

// src/app/(dashboard)/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  notifications,
  modal,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  notifications: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-12 gap-6 p-6">
      {/* メインコンテンツ */}
      <main className="col-span-8">{children}</main>

      {/* サイドパネル */}
      <aside className="col-span-4 space-y-6">
        <section className="bg-white rounded-lg shadow p-4">
          <h2 className="text-lg font-semibold mb-4">📊 アナリティクス</h2>
          {analytics}
        </section>
        <section className="bg-white rounded-lg shadow p-4">
          <h2 className="text-lg font-semibold mb-4">🔔 通知</h2>
          {notifications}
        </section>
      </aside>

      {/* モーダルスロット */}
      {modal}
    </div>
  );
}

Intercepting Routesでモーダルを実現

InstagramライクなモーダルUIを実装するパターンです。

// src/app/(dashboard)/dashboard/@modal/(..)projects/[id]/page.tsx
import { db } from '@/lib/db';
import { projects } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { Modal } from '@/components/modal';

interface Props {
  params: Promise<{ id: string }>;
}

export default async function ProjectModal({ params }: Props) {
  const { id } = await params;
  const project = await db.query.projects.findFirst({
    where: eq(projects.id, id),
  });

  if (!project) return null;

  return (
    <Modal>
      <h2 className="text-2xl font-bold">{project.name}</h2>
      <p className="mt-2 text-gray-600">{project.description}</p>
      <div className="mt-4 grid grid-cols-2 gap-4">
        <div className="bg-gray-50 p-3 rounded">
          <span className="text-sm text-gray-500">ステータス</span>
          <p className="font-semibold">{project.status}</p>
        </div>
        <div className="bg-gray-50 p-3 rounded">
          <span className="text-sm text-gray-500">メンバー</span>
          <p className="font-semibold">{project.memberCount}人</p>
        </div>
      </div>
    </Modal>
  );
}

キャッシュ戦略の最適化

Claude Codeにキャッシュを設計させる

Next.js App Routerの4層キャッシュは非常に複雑ですが、Claude Codeに適切なプロンプトを与えれば、最適なキャッシュ戦略を設計してくれます。

以下のページのキャッシュ戦略を設計して:
- トップページ: 1時間キャッシュ、ISR
- ユーザーダッシュボード: キャッシュなし、リアルタイム
- ブログ記事: ビルド時静的生成、更新時ISR
- 商品一覧: 5分キャッシュ、タグベースrevalidation
- 検索結果: キャッシュなし

Claude Codeが生成するキャッシュ設定:

// app/(public)/page.tsx — トップページ(ISR: 1時間)
export const revalidate = 3600;

export default async function HomePage() {
  const featured = await fetch('https://api.example.com/featured', {
    next: { revalidate: 3600 },
  }).then(r => r.json());

  return <FeaturedSection items={featured} />;
}

// app/(dashboard)/dashboard/page.tsx — ダッシュボード(動的)
export const dynamic = 'force-dynamic';

export default async function DashboardPage() {
  const stats = await db.query.stats.findMany({
    orderBy: desc(stats.updatedAt),
  });
  return <DashboardView stats={stats} />;
}

// app/(public)/blog/[slug]/page.tsx — ブログ(静的 + ISR)
export async function generateStaticParams() {
  const posts = await db.query.posts.findMany({
    columns: { slug: true },
  });
  return posts.map(p => ({ slug: p.slug }));
}

export const revalidate = 86400; // 24時間

// app/(public)/products/page.tsx — 商品一覧(タグベースrevalidation)
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { tags: ['products'], revalidate: 300 },
  }).then(r => r.json());

  return <ProductGrid products={products} />;
}

// 商品更新時のrevalidation
// app/_actions/product.ts
'use server';
import { revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: FormData) {
  await db.update(products).set({ /* ... */ }).where(eq(products.id, id));
  revalidateTag('products');
}

ミドルウェアと認証パターン

NextAuth.js v5との統合

認証フローはApp Routerで特に設計が重要な部分です。Claude Codeに以下のように指示すると、NextAuth.js v5との統合コードを生成してくれます。

NextAuth.js v5で認証を実装して。
- GitHub / Google OAuth
- ミドルウェアでダッシュボードを保護
- セッション情報をServer Componentから取得
- ロールベースアクセス制御(admin/member/viewer)
// middleware.ts
import { auth } from '@/lib/auth';
import { NextResponse } from 'next/server';

export default auth((req) => {
  const isAuthenticated = !!req.auth;
  const isAuthPage = req.nextUrl.pathname.startsWith('/auth');
  const isDashboard = req.nextUrl.pathname.startsWith('/dashboard');
  const isAdmin = req.nextUrl.pathname.startsWith('/admin');

  // 未認証ユーザーがダッシュボードにアクセス → ログインページ
  if (isDashboard && !isAuthenticated) {
    return NextResponse.redirect(new URL('/auth/signin', req.url));
  }

  // 管理者ページのアクセス制御
  if (isAdmin && req.auth?.user?.role !== 'admin') {
    return NextResponse.redirect(new URL('/dashboard', req.url));
  }

  // 認証済みユーザーがログインページにアクセス → ダッシュボード
  if (isAuthPage && isAuthenticated) {
    return NextResponse.redirect(new URL('/dashboard', req.url));
  }

  return NextResponse.next();
});

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/auth/:path*'],
};

SES現場でのNext.js App Router開発

よくある案件パターンとClaude Codeの活用

SES現場でNext.js App Routerを使う案件は増加傾向にあります。以下のパターンでClaude Codeが特に威力を発揮します。

1. レガシーPages Router → App Router移行

Pages RouterのこのコンポーネントをApp Routerに移行して。
getServerSideProps → async Server Component
useRouter → useRouter (next/navigation)
Link → Link (変更なし)
APIルート → Route Handler

2. 管理画面(ダッシュボード)開発

SES案件で最も多いパターンの一つ。Claude Codeで以下を一気に生成できます。

  • CRUD画面のフルセット(一覧・詳細・作成・編集・削除)
  • データテーブル(ソート・フィルタ・ページネーション)
  • フォームバリデーション(Zod + Server Actions)
  • 権限管理(ロールベース)

3. マルチテナントSaaS

マルチテナント対応のSaaSアプリ構造を作成して。
- /[tenant]/dashboard — テナント別ダッシュボード
- テナント解決はミドルウェアでサブドメインから
- テナントごとのDB分離(スキーマレベル)
- 共通管理画面(/admin)はスーパーアドミンのみ

単価への影響

Next.js App Routerのスキルは、SES市場で高い評価を得ています。

スキルセット月額単価相場(2026年)
React(基本)60〜75万円
Next.js Pages Router70〜85万円
Next.js App Router75〜95万円
App Router + RSC + Server Actions80〜100万円
上記 + Claude Code活用85〜110万円

Claude Code入門ガイドと合わせて、App Routerのスキルを身につけることで、SES市場での競争力を大幅に高められます。

よくあるプロンプトテンプレート集

CRUD画面生成

[モデル名]のCRUD画面をApp Routerで作成して。
- 一覧: Server Component、ページネーション、検索フィルタ付き
- 詳細: Server Component、関連データもfetch
- 作成/編集: Client Component、Server Action、Zodバリデーション
- 削除: Server Actionで確認ダイアログ付き
- loading.tsx / error.tsx / not-found.tsx も配置

API Route Handler生成

REST APIのRoute Handlerを作成して。
- GET /api/users — 一覧(ページネーション・フィルタ対応)
- GET /api/users/[id] — 詳細
- POST /api/users — 作成(Zodバリデーション)
- PATCH /api/users/[id] — 更新
- DELETE /api/users/[id] — 削除
- 各エンドポイントに認証チェック
- エラーレスポンスは統一フォーマット
- OpenAPI/Swagger型コメント付き

パフォーマンス最適化

このページのパフォーマンスを最適化して。
- 画像: next/imageに変換、適切なsizes設定
- フォント: next/fontでGoogleフォント最適化
- コンポーネント: dynamic importで遅延読み込み
- Suspense境界を適切に配置
- メタデータ: generateMetadata でSEO最適化

まとめ — App Router開発の生産性を最大化する

Claude Code × Next.js App Routerの組み合わせは、現代のフルスタック開発において最も生産性の高いワークフローの一つです。

  • ファイル規約の複雑さをClaude Codeが吸収し、正確なディレクトリ構造を自動生成
  • Server/Client境界の判断をAIが支援し、RSCのメリットを最大化
  • Server Actionsのボイラープレートを瞬時に生成し、型安全なデータ変更を実現
  • Parallel Routes・Intercepting Routesなどの高度なパターンもプロンプトで実装

Claude Codeフロントエンド開発ガイドTypeScript開発ガイドと組み合わせて学習を進め、SES現場でのNext.js App Router開発を効率化してください。Claude Codeのプロンプト設計術も合わせて参考にすることで、より精度の高いコード生成が可能になります。

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修