0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CLAUDE.md と .cursorrules に書く「アーキテクチャルール」完全ガイド——AIに設計を守らせる具体的な書き方

0
Posted at

CLAUDE.mdとcursorrulesでAIの設計を守らせる方法

AI コーディングツール(Cursor / Claude Code / GitHub Copilot)を使い始めると、よく起きる問題があります。「動くけど設計がカオス」です。

この記事では、なぜ AI がルールなしで混在したコードを書くのかを技術的に掘り下げ、CLAUDE.md.cursorrules に書く「アーキテクチャルール」の具体的な書き方をまとめます。

元記事(amanity.co.jp / ビジネス向けの解説)はこちらです。
https://amanity.co.jp/news/ai-driven-dev-mvc-rules


なぜ AI はルールなしで混在したコードを書くのか——技術的な説明

Next.js App Router が「境界線の消失」を引き起こした

従来の React + Express 構成では、ブラウザ側コードとサーバー側コードは物理的に別リポジトリ・別ファイルに分かれていました。DB クライアントをフロントに import することは、そもそも技術的に不可能か、明らかな逸脱として検知されていました。

Next.js App Router の page.tsx は React Server Component です。サーバー上で動くため、Prisma を直接 import して呼び出せます。この「API エンドポイントという強制的な境界」の消滅が、混在コードの温床を作りました。

// app/orders/[id]/page.tsx — AIが「ひとまず動く」として生成したコード
import { prisma } from '@/lib/prisma';

export default async function OrderPage({ params }: { params: { id: string } }) {
  // ① DBを直接呼ぶ
  const order = await prisma.order.findUnique({
    where: { id: params.id },
    include: { items: true, user: true },
  });

  if (!order) return <div>注文が見つかりません</div>;

  // ② ビジネスロジックもここに書く
  const subtotal = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const discount = order.user.isPremium ? subtotal * 0.1 : 0;
  const tax = (subtotal - discount) * 0.1;
  const total = subtotal - discount + tax;

  // ③ メール送信の副作用まで混入
  if (order.status === 'pending' && !order.reminderSent) {
    await fetch('/api/mail/send', {
      method: 'POST',
      body: JSON.stringify({ to: order.user.email, orderId: order.id }),
    });
    await prisma.order.update({
      where: { id: order.id },
      data: { reminderSent: true },
    });
  }

  return (
    <div>
      <h1>注文 #{order.id}</h1>
      <p>小計: {subtotal}</p>
      {totals.discount > 0 && <p>割引: -{discount}</p>}
      <p>消費税: {tax}</p>
      <p>合計: {total}</p>
    </div>
  );
}

このファイルは 4 つの責務を 1 か所で担っています。

# 責務 具体箇所
1 DB アクセス prisma.order.findUnique
2 ビジネスロジック 割引計算・消費税計算
3 副作用 メール送信・DB 更新
4 UI 描画 JSX の return

AI がこのコードを生成する理由——「暗黙の了解」を AI は学習していない

LLM の推論プロセスとして考えると分かりやすいです。

AI は「現在のコンテキスト内で最も確率の高いトークン列」を生成します。制約として機能するのは以下の 3 つです。

  1. プロンプトに書いた指示
  2. コンテキストウィンドウ内の既存コード
  3. 学習データに含まれる一般的なパターン

「page.tsx にはビジネスロジックを書かない」というルールは、どこにも書いていなければ上記の 3 つのどれにも含まれません。

Next.js の公式ドキュメントは「app/ ディレクトリを使いましょう」と書いていますが、「app/ に prisma を直接書いてはいけない」とは書いていません。学習データの中には責務を分離したコードも、混在したコードも存在します。

制約がない状態で「注文詳細ページを実装して」と依頼すると、AI は「技術的に動作するコードの中で最も短いもの」を選ぶ傾向があります。これが「ひとまず動く 1 ファイル全部入り」パターンです。

さらに悪いことに、コンテキストに混在したコードが含まれると、AI はそのパターンを踏襲します。AI が増やすのは、すでにそこにあるものです。


ルールファイルに何を書くか——最小限で効く書き方

ルールファイルの種類と読み込まれる仕組み

ツール ルールファイルのパス 読み込み範囲
Cursor .cursor/rules/*.mdc または .cursorrules プロジェクト全体(.mdc は glob で絞れる)
Claude Code CLAUDE.md 配置ディレクトリ以下
GitHub Copilot .github/copilot-instructions.md プロジェクト全体

Claude Code の場合、CLAUDE.md はサブディレクトリにも置けます。app/CLAUDE.md に「このディレクトリ内のルール」を書くと、そのディレクトリ配下の操作時のみ読み込まれます。大規模プロジェクトでは階層的に分けると管理しやすいです。

書くべき 3 要素

効果的なアーキテクチャルールには以下の 3 要素が必要です。

1. ディレクトリの責務(何を書いてよいか / 何を書いてはいけないか)

「何を書くか」だけでなく「何を書いてはいけないか」を明示することが重要です。AI は禁止されていなければ自由に選択します。

2. 依存の方向(どの層がどの層を import してよいか)

「A は B を import してよいが、B は A を import してはいけない」を明示します。これがないと AI は双方向で依存を作ります。

3. 禁止パターン(具体的な NG コードの pattern)

「〇〇を書かない」という具体的な記述。理想は「なぜか」の 1 行を添えることです。AI はルールの背景を理解すると文脈に応じて解釈できます。

Next.js + Prisma プロジェクト向けテンプレート

以下を CLAUDE.md または .cursorrules に貼れば、すぐに効果が出ます。

## アーキテクチャルール(必ず守ること)

### ディレクトリの責務

| パス | 書いてよいもの | 書いてはいけないもの |
|------|--------------|-------------------|
| `app/` | ルーティング・ページ構成・認証ガード | DB 直接アクセス・ビジネスロジック |
| `lib/*/repository.ts` | DB・外部 API との入出力のみ | 計算ロジック・バリデーション |
| `lib/*/service.ts` | ユースケース(複数リポジトリを組み合わせる処理) | UI 知識・Next.js 固有の型 |
| `lib/*/domain/*.ts` | 純粋なビジネスロジック(副作用なし・外部依存なし) | `import { NextRequest }` 等 |
| `components/` | 表示・インタラクション | `prisma` 直接使用・fetch(SWR を除く) |

### 依存の方向(逆転禁止)

app/ → lib//service.ts → lib//repository.ts → DB / 外部 API
app/ → components/(UI のみ)


Domain 層(`lib/*/domain/`)は他のどの層も import しない。
Repository 層は Domain 層を import してよいが、Service 層を import しない。

### 禁止パターン

- `app/` 配下のファイルに `prisma.〇〇.findXxx()` を直接書かない
  (理由: page.tsx はUIのみにし、テスト時にDBモックが不要な構造を維持するため)
- `lib/*/service.ts` に `import { NextRequest } from 'next/server'` を書かない
  (理由: Service 層を Next.js に依存させると別フレームワーク移行時に影響が広がるため)
- `lib/*/repository.ts` に割引計算・税計算などのビジネスロジックを書かない
  (理由: DB の都合とビジネスルールを分離し、ロジックの単体テストを可能にするため)
- `components/` から直接 `fetch('/api/...')` を書かない(SWR か Server Component 経由にする)
  (理由: クライアント側の副作用を一か所に集約し、キャッシュ・エラーハンドリングを統一するため)

### 新しいファイルを作るときのセルフチェック

1. 「このファイルは何をするファイルか」をひとことで言えるか?
2. UI 描画 / データアクセス / ビジネスロジック のどれか 1 つだけか?
3. 2 つ以上の責務が混在するなら、ファイルを分割してから実装する

Before / After——ルールを書いた場合の出力の違い

同じ「注文詳細ページを実装してください」という依頼に対して、上記ルールを読み込んだ状態では次のようなコードに分割されます。

After: repository.ts(DB アクセスのみ)

// lib/order/repository.ts
import { prisma } from '@/lib/prisma';

export async function getOrderById(id: string) {
  return prisma.order.findUnique({
    where: { id },
    include: { items: true, user: true },
  });
}

export async function markReminderSent(orderId: string) {
  return prisma.order.update({
    where: { id: orderId },
    data: { reminderSent: true },
  });
}

After: domain/pricing.ts(純粋なビジネスロジック)

// lib/order/domain/pricing.ts
import type { OrderItem, User } from '@prisma/client';

export function calculateOrderTotals(items: OrderItem[], user: User) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const discount = user.isPremium ? subtotal * 0.1 : 0;
  const tax = Math.floor((subtotal - discount) * 0.1);
  const total = subtotal - discount + tax;
  return { subtotal, discount, tax, total };
}

副作用ゼロ・外部依存ゼロの純粋関数なので、import するだけでユニットテストが書けます。

After: service.ts(ユースケース)

// lib/order/service.ts
import { getOrderById, markReminderSent } from './repository';
import { calculateOrderTotals } from './domain/pricing';

export async function getOrderWithTotals(id: string) {
  const order = await getOrderById(id);
  if (!order) return null;

  const totals = calculateOrderTotals(order.items, order.user);

  // 副作用はUIではなくここで担う
  if (order.status === 'pending' && !order.reminderSent) {
    await fetch('/api/mail/send', {
      method: 'POST',
      body: JSON.stringify({ to: order.user.email, orderId: order.id }),
    });
    await markReminderSent(order.id);
  }

  return { order, totals };
}

After: page.tsx(UI のみ)

// app/orders/[id]/page.tsx — UIのみ(10行以下)
import { getOrderWithTotals } from '@/lib/order/service';

export default async function OrderPage({ params }: { params: { id: string } }) {
  const result = await getOrderWithTotals(params.id);
  if (!result) return <div>注文が見つかりません</div>;

  const { order, totals } = result;

  return (
    <div>
      <h1>注文 #{order.id}</h1>
      <p>小計: {totals.subtotal}</p>
      {totals.discount > 0 && <p>割引: -{totals.discount}</p>}
      <p>消費税: {totals.tax}</p>
      <p>合計: {totals.total}</p>
    </div>
  );
}

変わったのは ルールファイルに書いたルールだけ です。依頼文も、AI も同じです。


ルールを書くときの 3 つのコツ

1. 禁止事項は「〜しない」の形で書く

推奨事項(「〜するとよい」)よりも禁止事項(「〜しない」)の方が AI の遵守率が高いです。LLM は制約に反応しやすいためです。

# 曖昧(効果薄)
- ビジネスロジックは domain 層に書くことを検討してください

# 明確(効果高)
- `app/``prisma` を直接 import しない
- `components/` から `fetch('/api/...')` を直接呼ばない

2. 理由を 1 行添える

AI はルールの背景を理解すると、明示されていないケースでも意図を推測できます。

# 理由なし(AI が例外を作りやすい)
- repository.ts にビジネスロジックを書かない

# 理由あり(AI が文脈を理解できる)
- repository.ts にビジネスロジックを書かない
  (理由: DB スキーマの変更とビジネスルールの変更を独立させるため。pricing.ts を変えても repository.ts に影響しない構造を維持する)

3. 大きくなったら別ファイルに分割して参照させる

CLAUDE.md が 300 行を超えてきたら設計ルールを ARCHITECTURE.md に分離し、CLAUDE.md から参照させます。

# CLAUDE.md
## アーキテクチャルール
詳細は `ARCHITECTURE.md` を参照してください。
禁止パターンの要約:
- `app/` に prisma を直接 import しない
- `service.ts` に Next.js の型を import しない

Claude Code は CLAUDE.md に書かれた参照先ファイルも自動で読み込みます。Cursor の .mdc ファイルは globs で対象ファイルを絞れるため、大規模プロジェクトでの管理が特に有効です。


MVC / Clean Architecture は「必須」か

この問いへの答えは「パターン名は問題ではない」です。

MVC でも Vertical Slice でも独自命名でも、AI にとって重要なのは「どこに何を書くか、何を書いてはいけないか」が明文化されているかどうかです。

Clean Architecture の「Use Case が Framework を知らない」というルールも、Vertical Slice の「features/order/ が features/payment/ の内部を直接 import しない」というルールも、本質は同じです。境界線と依存の方向が明文化されている。 それだけが問題です。

既存の設計パターンを使わなくてもよいです。チーム固有の命名でも、ルールが書いてあれば AI はそれに従います。逆に、MVC を採用していても CLAUDE.md にルールを書いていなければ、AI はMVCを守りません。


まとめ

AI に設計を守らせるために必要なことは 1 つです。

CLAUDE.md.cursorrules に、ディレクトリの責務・依存の方向・禁止パターンを書く。

今日からできるアクション:

  1. 使っているルールファイルに ## アーキテクチャルール セクションを追加する
  2. 「このファイルに prisma を書かない」という禁止事項を 1 行書く
  3. 理由を 1 行添える
  4. 同じタスクを AI に依頼して出力を比較する

最初は 3 行でよいです。「app/ に prisma を直接書かない」「ビジネスロジックは lib/ 配下の純粋関数に書く」「依存の方向は app → lib → DB」——これだけ書いて次のタスクを依頼してみてください。


元記事

本記事はこちらの元記事(ビジネス向け・設計パターン比較含む)をもとに、エンジニア向けに CLAUDE.md / .cursorrules の具体的な書き方に特化して執筆しました。

AI駆動開発でMVCは必要か——本当に重要なのはパターンより「明文化」だった
https://amanity.co.jp/news/ai-driven-dev-mvc-rules

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?