T-CREATOR

Cursor とクリーンアーキテクチャ:層境界を壊さない指示とファイル配置

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 を使用していると、以下のような問題に直面することがあります。

#問題具体例影響
1Domain 層から Infrastructure 層への依存Entity クラスに ORM の Decorator を追加ビジネスロジックが DB に依存
2UseCase から 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/ を明確に分ける
3Repository パターンで依存性逆転インターフェースは Domain 層、実装は Infrastructure 層に配置
4具体的な指示を心がけるファイルパスと制約を明示して Cursor に指示する
5DI コンテナで結合度を下げるtsyringe などの DI コンテナを活用する

Cursor は強力なツールですが、アーキテクチャの番人ではありません。開発者が原則を理解し、適切な指示と構成でプロジェクトを導くことが重要です。

.cursorrules ファイルでアーキテクチャ原則を伝え、明確なディレクトリ構造を維持し、具体的な指示を与えることで、Cursor は層境界を守りながら開発を加速してくれる最高のパートナーになりますよ。

この記事が、皆さんのプロジェクトでクリーンアーキテクチャを維持しながら AI 支援開発を活用する助けになれば幸いです。美しいアーキテクチャと高い生産性、その両立を目指していきましょう。

関連リンク