Cursor とクリーンアーキテクチャ:層境界を壊さない指示とファイル配置
AI 支援コードエディタの「Cursor」は、開発者の生産性を劇的に向上させる素晴らしいツールです。しかし、クリーンアーキテクチャを採用しているプロジェクトで Cursor を使う際、注意しないと層境界を破壊してしまう危険性があります。
この記事では、Cursor を使いながらもクリーンアーキテクチャの原則を守り、適切なファイル配置と指示方法を通じて、美しいアーキテクチャを維持する方法を解説します。Cursor を活用しつつ、長期的にメンテナンスしやすいコードベースを保つためのベストプラクティスをお届けしますね。
背景
クリーンアーキテクチャとは
クリーンアーキテクチャは、Uncle Bob(Robert C. Martin)が提唱したソフトウェア設計手法です。ビジネスロジックをフレームワークやデータベース、UI から独立させることで、テストしやすく、変更に強いシステムを実現できます。
クリーンアーキテクチャの基本原則は「依存性の方向」です。外側の層は内側の層に依存できますが、内側の層は外側の層を知りません。この一方向の依存関係こそが、システムの柔軟性を生み出す鍵になるのです。
以下の図で、クリーンアーキテクチャの層構造を確認しましょう。
mermaidflowchart TB
subgraph outer["外側の層"]
presentation["Presentation層<br/>UI・Controller"]
infrastructure["Infrastructure層<br/>DB・API・Framework"]
end
subgraph inner["内側の層"]
usecase["UseCase層<br/>アプリケーションロジック"]
domain["Domain層<br/>ビジネスロジック・Entity"]
end
presentation -->|依存| usecase
infrastructure -->|依存| usecase
usecase -->|依存| domain
style domain fill:#e1f5ff
style usecase fill:#fff4e1
style presentation fill:#ffe1e1
style infrastructure fill:#ffe1e1
図で理解できる要点:
- Domain 層が最も内側で、ビジネスロジックの中核を担います
- UseCase 層がアプリケーション固有の処理を定義します
- Presentation 層と Infrastructure 層は外側で、具体的な実装を担当します
Cursor の特徴と AI 支援開発
Cursor は VS Code ベースの AI ペアプログラミングツールで、コードの自動補完や生成、リファクタリングを支援してくれます。プロジェクト全体のコンテキストを理解し、適切なコードを提案する能力が非常に高いのが特徴ですね。
しかし、この「コンテキスト理解」には注意が必要です。Cursor は既存のコードパターンを学習して提案を行うため、アーキテクチャの原則を理解しているわけではありません。適切な指示がなければ、便利な提案が逆にアーキテクチャを破壊してしまう可能性があります。
課題
Cursor が層境界を壊してしまう問題
Cursor を使用していると、以下のような問題に直面することがあります。
| # | 問題 | 具体例 | 影響 |
|---|---|---|---|
| 1 | Domain 層から Infrastructure 層への依存 | Entity クラスに ORM の Decorator を追加 | ビジネスロジックが DB に依存 |
| 2 | UseCase から Presentation 層への依存 | UseCase が HTTP リクエストオブジェクトを直接受け取る | アプリケーションロジックが UI に依存 |
| 3 | 層を飛び越えた直接アクセス | Controller から Repository を直接呼び出す | UseCase の責務が不明確に |
これらの問題が発生する主な原因は、Cursor が「動くコード」を優先して提案するためです。「きれいなアーキテクチャ」よりも「すぐに動くコード」の方が AI にとって生成しやすいのです。
以下の図で、依存関係が壊れた状態を見てみましょう。
mermaidflowchart TB
controller["Controller<br/>(Presentation層)"]
usecase["UseCase<br/>(UseCase層)"]
entity["Entity<br/>(Domain層)"]
repository["Repository<br/>(Infrastructure層)"]
controller -->|正常| usecase
controller -.->|❌ 異常:層を飛び越え| repository
usecase -->|正常| entity
entity -.->|❌ 異常:内→外への依存| repository
repository -->|正常| entity
style controller fill:#ffe1e1
style usecase fill:#fff4e1
style entity fill:#e1f5ff
style repository fill:#ffe1e1
図で理解できる要点:
- 点線の矢印が「壊れた依存関係」を示しています
- Controller が UseCase を飛び越えて Repository を直接呼ぶのは NG です
- Entity(内側)が Repository(外側)に依存するのは原則違反です
具体的な失敗例
実際のコード例で、Cursor がどのように層境界を壊してしまうかを見てみましょう。
NG 例:Domain 層に Infrastructure の要素が混入
typescript// domain/entities/User.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
} from 'typeorm'; // ❌ ORM への依存
@Entity() // ❌ Infrastructure 層の Decorator
export class User {
@PrimaryGeneratedColumn() // ❌ Infrastructure 層の Decorator
id: number;
@Column() // ❌ Infrastructure 層の Decorator
name: string;
}
この例では、Domain 層の Entity が TypeORM(Infrastructure 層の ORM)に依存してしまっています。Cursor に「User エンティティを作って」と指示すると、TypeScript + Node.js のプロジェクトでは、このような提案をされることが多いのです。
NG 例:UseCase が Presentation 層に依存
typescript// usecases/CreateUserUseCase.ts
import { Request, Response } from 'express'; // ❌ Web フレームワークへの依存
export class CreateUserUseCase {
// ❌ Express の Request を直接受け取る
async execute(
req: Request,
res: Response
): Promise<void> {
const { name, email } = req.body;
// ... ユーザー作成処理
res.json({ success: true });
}
}
この例では、UseCase が Express(Presentation 層の Web フレームワーク)に依存しています。これでは、Web API を別のフレームワークに変更したり、CLI ツールから同じ UseCase を呼び出したりすることが困難になってしまいます。
解決策
.cursorrules による AI への指示
Cursor の挙動を制御する最も効果的な方法は、.cursorrules ファイルを活用することです。プロジェクトルートに .cursorrules ファイルを配置することで、Cursor にアーキテクチャの原則を教えることができます。
基本的な .cursorrules の構成
markdown# クリーンアーキテクチャ原則
# 依存関係のルール
- Domain 層は他のどの層にも依存しない
- UseCase 層は Domain 層のみに依存する
- Infrastructure 層と Presentation 層は UseCase 層と Domain 層に依存する
- 内側の層から外側の層への依存は絶対に禁止
より具体的に、各層での禁止事項を明記すると効果的です。
markdown# Domain 層(domain/)
## 許可されるもの
- ビジネスロジックのみ
- プレーンな TypeScript クラスとインターフェース
- 標準ライブラリのみ
## 禁止されるもの
- ORM の Decorator(@Entity, @Column など)
- Web フレームワークの import
- データベースクライアント
- 外部 API クライアント
同様に、UseCase 層、Infrastructure 層、Presentation 層についても定義していきましょう。
markdown# UseCase 層(usecases/)
## 許可されるもの
- アプリケーション固有のビジネスフロー
- Domain 層の Entity と Repository インターフェースの使用
- 入出力用の DTO(Data Transfer Object)
## 禁止されるもの
- HTTP リクエスト/レスポンスオブジェクト
- データベース接続
- 具体的な Repository 実装への依存
ファイル配置戦略
適切なファイル配置は、Cursor が正しいコンテキストを理解するために重要です。以下のようなディレクトリ構造を推奨します。
bashsrc/
├── domain/ # Domain 層
│ ├── entities/ # エンティティ
│ ├── repositories/ # Repository インターフェース
│ └── services/ # ドメインサービス
├── usecases/ # UseCase 層
│ ├── dto/ # データ転送オブジェクト
│ └── *.usecase.ts # ユースケース実装
├── infrastructure/ # Infrastructure 層
│ ├── database/ # DB 接続・マイグレーション
│ ├── repositories/ # Repository 実装
│ └── external/ # 外部 API クライアント
└── presentation/ # Presentation 層
├── controllers/ # コントローラー
├── middlewares/ # ミドルウェア
└── routes/ # ルーティング
図で理解できる要点:
- 層ごとにディレクトリを明確に分離します
- ファイル名の命名規則を統一します(.usecase.ts、.controller.ts など)
- Repository はインターフェース(domain)と実装(infrastructure)で分離します
Repository パターンと依存性逆転の原則
クリーンアーキテクチャの核心は「依存性逆転の原則(DIP)」です。Domain 層が Infrastructure 層を知らないようにするため、Repository パターンを活用します。
以下の図で、依存性逆転の仕組みを確認しましょう。
mermaidflowchart TB
subgraph domain_layer["Domain 層"]
entity["User Entity"]
repo_interface["IUserRepository<br/>(インターフェース)"]
end
subgraph usecase_layer["UseCase 層"]
usecase["CreateUserUseCase"]
end
subgraph infrastructure_layer["Infrastructure 層"]
repo_impl["UserRepository<br/>(実装クラス)"]
db[("Database")]
end
usecase -->|使用| repo_interface
usecase -->|使用| entity
repo_impl -.->|実装| repo_interface
repo_impl -->|操作| db
repo_impl -->|返却| entity
style entity fill:#e1f5ff
style repo_interface fill:#e1f5ff
style usecase fill:#fff4e1
style repo_impl fill:#ffe1e1
図で理解できる要点:
- Repository のインターフェースは Domain 層に配置します
- 実装クラスは Infrastructure 層に配置します
- UseCase は具体的な実装ではなく、インターフェースに依存します
Domain 層:Repository インターフェース定義
typescript// domain/repositories/IUserRepository.ts
import { User } from '../entities/User';
// Repository のインターフェースを定義
export interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<User>;
delete(id: string): Promise<void>;
}
このインターフェースは、Domain 層で定義されますが、実装は持ちません。これにより、Domain 層が Infrastructure 層に依存しない状態を保てます。
Infrastructure 層:Repository 実装
typescript// infrastructure/repositories/UserRepository.ts
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { User } from '../../domain/entities/User';
import { PrismaClient } from '@prisma/client';
// インターフェースを実装
export class UserRepository implements IUserRepository {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> {
// Prisma を使った実装
const userData = await this.prisma.user.findUnique({
where: { id }
});
if (!userData) return null;
// DB のデータを Domain の Entity に変換
return new User(userData.id, userData.name, userData.email);
}
ここで重要なのは、実装クラスが Domain 層のインターフェースを実装している点です。依存の方向は「Infrastructure → Domain」となり、逆転が実現されています。
typescript async save(user: User): Promise<User> {
const userData = await this.prisma.user.create({
data: {
id: user.id,
name: user.name,
email: user.email
}
});
return new User(userData.id, userData.name, userData.email);
}
async delete(id: string): Promise<void> {
await this.prisma.user.delete({
where: { id }
});
}
}
Cursor への具体的な指示方法
Cursor に作業を依頼する際は、以下のような明確な指示を心がけましょう。
悪い指示例:
ユーザー作成機能を追加して
この指示では、Cursor は最も簡単な実装を選択してしまい、層境界を無視する可能性があります。
良い指示例:
markdown以下の手順でユーザー作成機能を実装してください:
1. domain/entities/User.ts に User エンティティを作成(純粋な TypeScript クラス、ORM の Decorator は使わない)
2. domain/repositories/IUserRepository.ts にインターフェースを定義
3. usecases/CreateUserUseCase.ts に UseCase を実装(IUserRepository に依存)
4. infrastructure/repositories/UserRepository.ts に実装クラスを作成(Prisma を使用)
5. presentation/controllers/UserController.ts にコントローラーを実装
依存関係は必ず Domain ← UseCase ← Infrastructure/Presentation の方向を守ってください。
このように具体的なファイルパスと制約を明示することで、Cursor は適切なコードを生成しやすくなります。
DI コンテナの活用
依存性の注入(DI)を適切に行うため、DI コンテナの使用を推奨します。これにより、各層の結合度を下げ、テストしやすいコードを実現できますね。
DI コンテナの設定例(tsyringe を使用)
typescript// infrastructure/di/container.ts
import 'reflect-metadata';
import { container } from 'tsyringe';
import { PrismaClient } from '@prisma/client';
// Infrastructure 層の実装をコンテナに登録
import { UserRepository } from '../repositories/UserRepository';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
インターフェースと実装クラスの紐付けを行います。
typescript// Prisma クライアントの登録
container.registerSingleton(PrismaClient);
// Repository の登録(インターフェースと実装の紐付け)
container.register<IUserRepository>('IUserRepository', {
useClass: UserRepository,
});
UseCase での DI の活用
typescript// usecases/CreateUserUseCase.ts
import { inject, injectable } from 'tsyringe';
import { IUserRepository } from '../domain/repositories/IUserRepository';
import { User } from '../domain/entities/User';
@injectable()
export class CreateUserUseCase {
// インターフェースを inject
constructor(
@inject('IUserRepository')
private userRepository: IUserRepository
) {}
このように、UseCase は具体的な実装クラスを知らず、インターフェースのみに依存します。
typescript async execute(input: CreateUserInput): Promise<CreateUserOutput> {
// ビジネスロジックの検証
if (!input.email.includes('@')) {
throw new Error('Invalid email format');
}
// Entity の生成
const user = new User(
crypto.randomUUID(),
input.name,
input.email
);
// Repository を通じて永続化
const savedUser = await this.userRepository.save(user);
return {
userId: savedUser.id,
name: savedUser.name,
email: savedUser.email
};
}
}
具体例
プロジェクト構成の全体像
実際のプロジェクトで、クリーンアーキテクチャを適用した構成を見ていきましょう。
graphqlmy-clean-app/
├── .cursorrules # Cursor への指示ファイル
├── src/
│ ├── domain/
│ │ ├── entities/
│ │ │ └── User.ts # ユーザーエンティティ
│ │ ├── repositories/
│ │ │ └── IUserRepository.ts # Repository インターフェース
│ │ └── services/
│ │ └── UserDomainService.ts # ドメインサービス
│ ├── usecases/
│ │ ├── dto/
│ │ │ ├── CreateUserInput.ts # 入力 DTO
│ │ │ └── CreateUserOutput.ts # 出力 DTO
│ │ ├── CreateUserUseCase.ts
│ │ └── GetUserUseCase.ts
│ ├── infrastructure/
│ │ ├── database/
│ │ │ ├── prisma/
│ │ │ │ └── schema.prisma # DB スキーマ
│ │ │ └── PrismaClient.ts
│ │ ├── repositories/
│ │ │ └── UserRepository.ts # Repository 実装
│ │ └── di/
│ │ └── container.ts # DI コンテナ設定
│ └── presentation/
│ ├── controllers/
│ │ └── UserController.ts # コントローラー
│ ├── middlewares/
│ │ └── errorHandler.ts # エラーハンドリング
│ └── routes/
│ └── userRoutes.ts # ルーティング
└── tests/ # テストコード
├── domain/
├── usecases/
└── integration/
以下の図で、各層間のデータフローを確認しましょう。
mermaidsequenceDiagram
participant Client as クライアント
participant Controller as UserController<br/>(Presentation)
participant UseCase as CreateUserUseCase<br/>(UseCase)
participant Entity as User<br/>(Domain)
participant Repo as UserRepository<br/>(Infrastructure)
participant DB as Database
Client->>Controller: POST /users
Controller->>Controller: リクエスト検証
Controller->>UseCase: execute(CreateUserInput)
UseCase->>Entity: new User()
UseCase->>Repo: save(user)
Repo->>DB: INSERT
DB-->>Repo: 保存完了
Repo-->>UseCase: User エンティティ
UseCase-->>Controller: CreateUserOutput
Controller-->>Client: JSON レスポンス
図で理解できる要点:
- リクエストは外側(Controller)から内側(UseCase → Entity)へ流れます
- データの永続化は Repository を通じて行われます
- レスポンスは DTO に変換されて返されます
Domain 層の実装例
純粋な Entity クラス
typescript// domain/entities/User.ts
// 外部ライブラリに依存しない純粋なクラス
export class User {
constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string,
public readonly createdAt: Date = new Date()
) {
this.validate();
}
Entity は、ビジネスルールのバリデーションを含めます。
typescript // ビジネスルールのバリデーション
private validate(): void {
if (!this.name || this.name.trim().length === 0) {
throw new Error('User name cannot be empty');
}
if (!this.isValidEmail(this.email)) {
throw new Error('Invalid email format');
}
}
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
ビジネスロジックをメソッドとして定義します。
typescript // ビジネスロジック
public canSendEmail(): boolean {
// メール送信可能かのビジネスルール
return this.email !== null && this.isValidEmail(this.email);
}
public updateName(newName: string): User {
// 名前変更時は新しい User インスタンスを返す(Immutable)
return new User(this.id, newName, this.email, this.createdAt);
}
}
UseCase 層の実装例
入力・出力 DTO の定義
typescript// usecases/dto/CreateUserInput.ts
// UseCase への入力データ
export interface CreateUserInput {
name: string;
email: string;
}
typescript// usecases/dto/CreateUserOutput.ts
// UseCase からの出力データ
export interface CreateUserOutput {
userId: string;
name: string;
email: string;
createdAt: Date;
}
UseCase の実装
typescript// usecases/CreateUserUseCase.ts
import { injectable, inject } from 'tsyringe';
import { IUserRepository } from '../domain/repositories/IUserRepository';
import { User } from '../domain/entities/User';
import { CreateUserInput } from './dto/CreateUserInput';
import { CreateUserOutput } from './dto/CreateUserOutput';
@injectable()
export class CreateUserUseCase {
constructor(
@inject('IUserRepository')
private userRepository: IUserRepository
) {}
UseCase は、アプリケーション固有のビジネスフローを実装します。
typescript async execute(input: CreateUserInput): Promise<CreateUserOutput> {
// 1. 入力値の検証(アプリケーション層のバリデーション)
if (!input.name || !input.email) {
throw new Error('Name and email are required');
}
// 2. Domain Entity の生成(ドメイン層のバリデーションが実行される)
const user = new User(
this.generateUserId(),
input.name,
input.email
);
// 3. Repository を通じた永続化
const savedUser = await this.userRepository.save(user);
DTO に変換して結果を返します。
typescript // 4. 出力 DTO への変換
return {
userId: savedUser.id,
name: savedUser.name,
email: savedUser.email,
createdAt: savedUser.createdAt
};
}
private generateUserId(): string {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
Infrastructure 層の実装例
Repository の実装クラス
typescript// infrastructure/repositories/UserRepository.ts
import { injectable } from 'tsyringe';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { User } from '../../domain/entities/User';
import { PrismaClient } from '@prisma/client';
@injectable()
export class UserRepository implements IUserRepository {
constructor(private prisma: PrismaClient) {}
DB のデータを Domain Entity に変換する処理を実装します。
typescript async findById(id: string): Promise<User | null> {
// Prisma を使った DB アクセス
const userData = await this.prisma.user.findUnique({
where: { id }
});
if (!userData) {
return null;
}
// DB のデータを Domain Entity に変換
return this.toDomain(userData);
}
Entity を DB 用のデータ形式に変換する処理も必要です。
typescript async save(user: User): Promise<User> {
// Domain Entity を DB 用のデータに変換
const userData = await this.prisma.user.create({
data: {
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt
}
});
return this.toDomain(userData);
}
async delete(id: string): Promise<void> {
await this.prisma.user.delete({
where: { id }
});
}
データ変換のヘルパーメソッドを定義します。
typescript // DB データから Domain Entity への変換
private toDomain(data: any): User {
return new User(
data.id,
data.name,
data.email,
data.createdAt
);
}
}
Presentation 層の実装例
Controller の実装
typescript// presentation/controllers/UserController.ts
import { Request, Response, NextFunction } from 'express';
import { container } from 'tsyringe';
import { CreateUserUseCase } from '../../usecases/CreateUserUseCase';
import { CreateUserInput } from '../../usecases/dto/CreateUserInput';
export class UserController {
async createUser(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
Controller は HTTP リクエストを DTO に変換します。
typescript// HTTP リクエストから DTO への変換
const input: CreateUserInput = {
name: req.body.name,
email: req.body.email,
};
// UseCase の実行
const useCase = container.resolve(CreateUserUseCase);
const output = await useCase.execute(input);
DTO を HTTP レスポンスに変換して返します。
typescript // DTO から HTTP レスポンスへの変換
res.status(201).json({
success: true,
data: {
userId: output.userId,
name: output.name,
email: output.email,
createdAt: output.createdAt
}
});
} catch (error) {
next(error); // エラーハンドリングミドルウェアに委譲
}
}
}
Cursor への指示例(実践的な会話)
実際に Cursor を使用する際の会話例を紹介します。
シナリオ 1:新機能追加時の指示
markdownユーザーのメールアドレス変更機能を追加したいです。以下の原則を守って実装してください:
【アーキテクチャ原則】
- Domain 層は他の層に依存しない
- UseCase 層は Domain 層のみに依存
- Repository パターンで依存性を逆転
【実装手順】
1. domain/entities/User.ts に updateEmail メソッドを追加
2. domain/repositories/IUserRepository.ts に update メソッドを追加
3. usecases/UpdateUserEmailUseCase.ts を新規作成
4. infrastructure/repositories/UserRepository.ts に update メソッドを実装
5. presentation/controllers/UserController.ts に updateEmail メソッドを追加
【制約】
- Entity は Immutable にすること
- UseCase は HTTP の概念を含まないこと
- エラーハンドリングは適切に行うこと
このように具体的に指示することで、Cursor は層境界を守った実装を提案してくれます。
シナリオ 2:リファクタリング時の指示
markdown現在の UserController が直接 Repository を呼び出しています。
これをクリーンアーキテクチャに沿ってリファクタリングしてください。
【現状】
- presentation/controllers/UserController.ts が infrastructure/repositories/UserRepository.ts を直接使用している
【目標】
- UseCase 層を間に挟んで、Controller は UseCase のみに依存する
- Repository は IUserRepository インターフェースを通じて使用する
【手順】
1. GetUserUseCase を新規作成(usecases/GetUserUseCase.ts)
2. UserController を修正し、GetUserUseCase を使用するように変更
3. DI コンテナの設定を確認し、必要なら更新
既存の動作は変更しないこと。
テスト戦略
クリーンアーキテクチャの大きな利点は、テストのしやすさです。各層を独立してテストできます。
Domain 層のテスト(Unit Test)
typescript// tests/domain/entities/User.test.ts
import { User } from '../../../src/domain/entities/User';
describe('User Entity', () => {
describe('constructor', () => {
test('正常な値で User インスタンスが生成される', () => {
const user = new User('123', 'John Doe', 'john@example.com');
expect(user.id).toBe('123');
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
});
バリデーションのテストも重要です。
typescript test('無効なメールアドレスでエラーが発生する', () => {
expect(() => {
new User('123', 'John Doe', 'invalid-email');
}).toThrow('Invalid email format');
});
test('空の名前でエラーが発生する', () => {
expect(() => {
new User('123', '', 'john@example.com');
}).toThrow('User name cannot be empty');
});
});
});
UseCase 層のテスト(Mock を使用)
typescript// tests/usecases/CreateUserUseCase.test.ts
import { CreateUserUseCase } from '../../src/usecases/CreateUserUseCase';
import { IUserRepository } from '../../src/domain/repositories/IUserRepository';
import { User } from '../../src/domain/entities/User';
describe('CreateUserUseCase', () => {
let useCase: CreateUserUseCase;
let mockRepository: jest.Mocked<IUserRepository>;
beforeEach(() => {
// Repository の Mock を作成
mockRepository = {
findById: jest.fn(),
save: jest.fn(),
delete: jest.fn()
};
Mock を使うことで、Infrastructure 層に依存せずテストできます。
typescript useCase = new CreateUserUseCase(mockRepository);
});
test('正常にユーザーが作成される', async () => {
// Mock の動作を定義
const savedUser = new User('123', 'John', 'john@example.com');
mockRepository.save.mockResolvedValue(savedUser);
// UseCase の実行
const result = await useCase.execute({
name: 'John',
email: 'john@example.com'
});
// 検証
expect(mockRepository.save).toHaveBeenCalledTimes(1);
expect(result.userId).toBe('123');
expect(result.name).toBe('John');
});
});
まとめ
Cursor とクリーンアーキテクチャの組み合わせは、正しく扱えば開発効率を大幅に向上させることができます。この記事で紹介した手法を振り返ってみましょう。
重要なポイント:
| # | ポイント | 実践方法 |
|---|---|---|
| 1 | .cursorrules で原則を明示 | 依存関係のルールを明確に記述する |
| 2 | 層ごとにディレクトリを分離 | domain/, usecases/, infrastructure/, presentation/ を明確に分ける |
| 3 | Repository パターンで依存性逆転 | インターフェースは Domain 層、実装は Infrastructure 層に配置 |
| 4 | 具体的な指示を心がける | ファイルパスと制約を明示して Cursor に指示する |
| 5 | DI コンテナで結合度を下げる | tsyringe などの DI コンテナを活用する |
Cursor は強力なツールですが、アーキテクチャの番人ではありません。開発者が原則を理解し、適切な指示と構成でプロジェクトを導くことが重要です。
.cursorrules ファイルでアーキテクチャ原則を伝え、明確なディレクトリ構造を維持し、具体的な指示を与えることで、Cursor は層境界を守りながら開発を加速してくれる最高のパートナーになりますよ。
この記事が、皆さんのプロジェクトでクリーンアーキテクチャを維持しながら AI 支援開発を活用する助けになれば幸いです。美しいアーキテクチャと高い生産性、その両立を目指していきましょう。
関連リンク
- Clean Architecture(Robert C. Martin) - クリーンアーキテクチャの原典
- Cursor 公式サイト - Cursor の公式ドキュメント
- .cursorrules ガイド - .cursorrules ファイルの作成ガイド
- TSyringe(TypeScript DI コンテナ) - Microsoft が開発する軽量 DI コンテナ
- Prisma(ORM) - TypeScript 向けの型安全な ORM
- 依存性逆転の原則(DIP) - SOLID 原則の D
- Repository パターン - Martin Fowler による Repository パターンの解説
articleCursor とクリーンアーキテクチャ:層境界を壊さない指示とファイル配置
articleCursor デバッグ指示テンプレ:最小再現・ログ挿入・原因仮説の書き方
articleCursor × Monorepo 構築:Yarn Workspaces/Turborepo/tsconfig path のベストプラクティス
articleGitHub Copilot Workspace と Cursor/Cline の比較検証:仕様駆動の自動化能力はどこまで?
articleCursor の自動テスト生成を検証:Vitest/Jest/Playwright のカバレッジ実測
articleCursor で差分が崩れる/意図しない大量変更が入るときの復旧プレイブック
articleHaystack で最小の検索 QA を作る:Retriever + Reader の 30 分ハンズオン
articleJest のフレークテスト撲滅作戦:重試行・乱数固定・リトライ設計の実務
articleGitHub Copilot セキュア運用チェックリスト:権限・ポリシー・ログ・教育の定着
articleGrok で社内 FAQ ボット:ナレッジ連携・権限制御・改善サイクル
articleGitHub Actions ランナーのオートスケール運用:Kubernetes/actions-runner-controller 実践
articleClips AI で書き出しが止まる時の原因切り分け:メモリ不足・コーデック・権限
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来