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 つです。
- プロンプトに書いた指示
- コンテキストウィンドウ内の既存コード
- 学習データに含まれる一般的なパターン
「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に、ディレクトリの責務・依存の方向・禁止パターンを書く。
今日からできるアクション:
- 使っているルールファイルに
## アーキテクチャルールセクションを追加する - 「このファイルに prisma を書かない」という禁止事項を 1 行書く
- 理由を 1 行添える
- 同じタスクを AI に依頼して出力を比較する
最初は 3 行でよいです。「app/ に prisma を直接書かない」「ビジネスロジックは lib/ 配下の純粋関数に書く」「依存の方向は app → lib → DB」——これだけ書いて次のタスクを依頼してみてください。
元記事
本記事はこちらの元記事(ビジネス向け・設計パターン比較含む)をもとに、エンジニア向けに CLAUDE.md / .cursorrules の具体的な書き方に特化して執筆しました。
AI駆動開発でMVCは必要か——本当に重要なのはパターンより「明文化」だった
https://amanity.co.jp/news/ai-driven-dev-mvc-rules
