5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

シリーズ: AI時代におけるフロントエンド(NextJS/ReactJS)のディレクトリ構造

Posted at

パート2: Clean Architecture - AI時代におけるコード組織化の解決策

Screenshot 2025-11-24 at 13.45.23.png

1. はじめに: 実際の問題と解決策

プロジェクトの背景

私は大規模なフロントエンドプロジェクトで作業しています:

  • チームサイズ: 10人以上の開発者
  • コードベース: 1000以上のファイル
  • 作業量: 毎日20以上のPRをレビュー

問題: AI使用時のコードの非同期

チームの全メンバーが開発速度を向上させるためにAIアシスタント(Cursor、Claude、GitHub Copilot)を使用しています。しかし、深刻な問題に直面しています:

  • コードの非同期: 各開発者が異なるコード組織方法を持ち、AIが異なるパターンでコードを提案する
  • 保守が困難: ビジネスロジックが散在し、コードをどこに配置すべきかわからない
  • 拡張が困難: 新機能を追加するために多くの場所を修正する必要があり、コンフリクトが発生しやすい
  • レビューが遅い: コードがどのレイヤーに属するかわからず、レビューに時間がかかる

解決策: Clean Architecture + AI Rules

Clean Architectureと.cursorrulesファイルの組み合わせで問題を解決しました:

  • コードの同期: すべての開発者とAIが同じアーキテクチャパターンに従う
  • 保守が容易: 構造が明確で、コードを正確にどこに配置すべきかわかる
  • 拡張が容易: 新機能を追加しても既存のコードに影響しない
  • レビューが速い: 各ファイルがどのレイヤーに属するか明確で、レビューが効率的

記事の目的

この記事では、Next.js 16とStripeを使用したeコマースプロジェクトにClean Architectureを適用した方法を共有します。以下を含みます:

  • ディレクトリ構造とレイヤー
  • コードルールとベストプラクティス
  • AIがアーキテクチャを理解するための.cursorrulesファイル
  • 実際のパターンとフロー
  • AIに優しいコードのためのチェックリスト

注意: この記事は、AI時代におけるフロントエンドアーキテクチャに関するシリーズの第1部です。コード例は理解しやすく簡略化されていますが、コア原則を正確に反映しています。

目次

  1. はじめに: 実際の問題と解決策
  2. プロジェクトの背景と使用技術
    • 2.1. プロジェクトの背景
    • 2.2. 使用技術
  3. フロントエンドにおけるClean Architectureの説明と設計
    • 3.1. Clean Architectureとは?
    • 3.2. プロジェクトのディレクトリ構造
    • 3.3. フロントエンドにおけるレイヤー構造
    • 3.4. Dependency Injection (DI)
    • 3.5. 依存関係ルール
    • 3.6. 完全なリクエスト処理フロー
  4. 長所と短所
    • 4.1. 長所
    • 4.2. 短所
    • 4.3. Clean Architectureを使用すべきタイミング
  5. コードルールとベストプラクティス
    • 5.1. コードルールチェックリスト
    • 5.2. 一般的なフローとパターン
    • 5.3. 避けるべきアンチパターン
  6. AIに優しいベストプラクティス(Cursor、Claude、GitHub Copilot)
    • 6.0. AI用のファイルルール: .cursorrules
    • 6.1. コードドキュメントとコメント
    • 6.2. 明確な命名規則
    • 6.3. 詳細な型定義
    • 6.4. コード構造と組織化
    • 6.5. コンテキストと例
    • 6.6. AIに優しいコードパターン
    • 6.7. ツールと自動化
    • 6.8. まとめ: AIに優しいコードのチェックリスト
  7. 結論

2. プロジェクトの背景と使用技術

2.1. プロジェクトの背景

私たちのプロジェクトは、ユーザーが以下を実行できるeコマースアプリケーションです:

  • 製品の閲覧: 製品リストの表示、検索、カテゴリ別フィルタリング
  • カート管理: 製品の追加/削除、数量の更新
  • 注文: 配送情報を含む注文の作成
  • 決済: Stripeを統合して安全な決済処理
  • 注文管理: 注文履歴の表示、ステータスの追跡

これは、多くのビジネスロジック、外部サービス(Stripe、メールサービス)との統合、決済処理時の高いセキュリティを確保する必要がある複雑なプロジェクトです。

2.2. 使用技術

コアフレームワークと言語

  • Next.js 16: App Router、Server Components、Server Actionsを備えたReactフレームワーク
  • TypeScript 5: 型安全性と優れた開発者体験
  • React 18: Server Componentsサポートを備えたUIライブラリ

UIとスタイリング

  • Shadcn UI: Radix UIベースのコンポーネントライブラリ
  • Tailwind CSS: ユーティリティファーストのCSSフレームワーク
  • React Hook Form: フォーム状態とバリデーションの管理
  • Zod: TypeScript用のスキーマバリデーション

状態管理とデータ

  • Drizzle ORM: 型安全なSQLクエリビルダー
  • Lucia Auth: 認証ライブラリ
  • Dependency Injection: IoCコンテナに@evyweb/ioctopusを使用

決済統合

  • Stripe: 決済処理
  • Stripe Elements: セキュアな決済フォームコンポーネント

開発ツール

  • ESLint: アーキテクチャルールを強制するeslint-plugin-boundariesを使用したコードリンティング
  • Prettier: コードフォーマット
  • Knip: 未使用コードと依存関係の検出
  • Vitest: ユニットテストフレームワーク
  • Commitlint: コンベンショナルコミットの強制
  • Husky: Gitフック

モニタリングとエラートラッキング

  • Sentry: エラートラッキングとパフォーマンスモニタリング

3. フロントエンドにおけるClean Architectureの説明と設計

3.1. Clean Architectureとは?

Clean Architectureは、関心の分離(separation of concerns)の原則に基づいたコード組織化の方法です。このアーキテクチャは、明確な責任と厳格な依存関係ルールを持つレイヤー(層)にアプリケーションを分割します。

Clean Architecture Diagram

出典: Clean Architecture by Uncle Bob

Clean Architectureの主な目標:

  • フレームワークからの独立性: ビジネスロジックがReact、Next.js、または任意のフレームワークに依存しない
  • UIからの独立性: WebからモバイルにUIを変更してもビジネスロジックに影響しない
  • データベースからの独立性: SQLiteからPostgreSQLに移行してもユースケースを変更する必要がない
  • テスト可能: ビジネスロジックをUIやデータベースをモックせずに独立してテストできる
  • 外部サービスからの独立性: 決済プロバイダーをStripeからPayPalに変更してもコアロジックに影響しない

3.2. プロジェクトのディレクトリ構造

各レイヤーを詳しく見る前に、プロジェクトの実際のディレクトリ構造を見てみましょう:

nextjs-16-clean-architecture/
├── app/                                    # Frameworks & Drivers Layer
│   ├── (auth)/                            # 認証用のルートグループ
│   │   ├── sign-in/
│   │   │   └── page.tsx                   # サインインページ
│   │   ├── sign-up/
│   │   │   └── page.tsx                   # サインアップページ
│   │   └── actions.ts                     # 認証用のServer Actions
│   ├── _components/                       # 共有UIコンポーネント
│   │   ├── ui/                            # Shadcn UIコンポーネント
│   │   │   ├── button.tsx
│   │   │   ├── input.tsx
│   │   │   ├── card.tsx
│   │   │   └── ...
│   │   ├── theme-provider.tsx
│   │   └── utils.ts                       # ユーティリティ関数(cnなど)
│   ├── actions.ts                         # Server Actions
│   ├── layout.tsx                         # ルートレイアウト
│   ├── page.tsx                           # ホームページ
│   └── globals.css                        # グローバルスタイル
│
├── src/                                    # コアアプリケーションコード
│   ├── entities/                          # Entities Layer (Domain)
│   │   ├── models/                        # ドメインモデル
│   │   │   ├── order.ts                   # 注文ドメインモデル
│   │   │   ├── product.ts                 # 製品ドメインモデル
│   │   │   ├── user.ts                    # ユーザードメインモデル
│   │   │   └── ...
│   │   └── errors/                        # カスタムドメインエラー
│   │       ├── orders.ts                  # 注文関連エラー
│   │       ├── payment.ts                # 決済関連エラー
│   │       └── common.ts                  # 共通エラー
│   │
│   ├── application/                       # Application Layer
│   │   ├── use-cases/                    # ビジネスユースケース
│   │   │   ├── orders/
│   │   │   │   ├── create-order.use-case.ts
│   │   │   │   ├── cancel-order.use-case.ts
│   │   │   │   └── get-orders-for-user.use-case.ts
│   │   │   ├── payment/
│   │   │   │   ├── process-payment.use-case.ts
│   │   │   │   └── refund-payment.use-case.ts
│   │   │   └── auth/
│   │   │       ├── sign-in.use-case.ts
│   │   │       ├── sign-up.use-case.ts
│   │   │       └── sign-out.use-case.ts
│   │   ├── repositories/                 # リポジトリインターフェース
│   │   │   ├── orders.repository.interface.ts
│   │   │   ├── products.repository.interface.ts
│   │   │   └── users.repository.interface.ts
│   │   └── services/                      # サービスインターフェース
│   │       ├── payment.service.interface.ts
│   │       ├── authentication.service.interface.ts
│   │       └── instrumentation.service.interface.ts
│   │
│   ├── infrastructure/                    # Infrastructure Layer
│   │   ├── repositories/                 # リポジトリ実装
│   │   │   ├── orders.repository.ts      # 注文用のデータベース操作
│   │   │   ├── orders.repository.mock.ts # テスト用のモック
│   │   │   ├── products.repository.ts
│   │   │   └── users.repository.ts
│   │   └── services/                      # サービス実装
│   │       ├── payment.service.ts        # Stripe統合
│   │       ├── payment.service.mock.ts   # テスト用のモック
│   │       ├── authentication.service.ts
│   │       └── instrumentation.service.ts
│   │
│   └── interface-adapters/               # Interface Adapters Layer
│       └── controllers/                  # コントローラー
│           ├── orders/
│           │   ├── create-order.controller.ts
│           │   ├── cancel-order.controller.ts
│           │   └── get-orders-for-user.controller.ts
│           ├── payment/
│           │   ├── process-payment.controller.ts
│           │   └── refund-payment.controller.ts
│           └── auth/
│               ├── sign-in.controller.ts
│               ├── sign-up.controller.ts
│               └── sign-out.controller.ts
│
├── di/                                     # Dependency Injection
│   ├── container.ts                       # DIコンテナのセットアップ
│   ├── types.ts                           # DIシンボル/型
│   └── modules/                           # DIモジュール
│       ├── orders.module.ts              # 注文DI設定
│       ├── payment.module.ts            # 決済DI設定
│       ├── authentication.module.ts     # 認証DI設定
│       └── ...
│
├── drizzle/                               # データベース
│   ├── schema.ts                         # データベーススキーマ定義
│   ├── index.ts                          # データベースクライアントのセットアップ
│   └── migrations/                       # データベースマイグレーション
│
├── tests/                                 # ユニットテスト
│   └── unit/
│       ├── application/
│       │   └── use-cases/
│       ├── infrastructure/
│       │   └── repositories/
│       └── interface-adapters/
│           └── controllers/
│
├── docs/                                  # ドキュメント
│   ├── clean-architecture-article.md     # この記事
│   └── conventional-commits.md           # コミット規約
│
├── .eslintrc.json                         # boundariesプラグインを使用したESLint設定
├── .prettierrc.json                       # Prettier設定
├── commitlint.config.js                   # Commitlint設定
├── knip.json                              # Knip設定
├── next.config.mjs                        # Next.js設定
├── tailwind.config.ts                     # Tailwind設定
├── tsconfig.json                          # TypeScript設定
└── package.json                           # 依存関係

構造に関する重要なポイント

  1. app/: Next.jsに関連するすべてのコード(ページ、コンポーネント、Server Actions)を含む。これは最外層で、フレームワークに依存する。

  2. src/: すべてのビジネスロジックを含み、Clean Architectureレイヤーで組織化される:

    • entities/: ドメインモデルとビジネスルール(何にも依存しない)
    • application/: ユースケースとインターフェース(entitiesに依存)
    • infrastructure/: 実装(applicationとentitiesに依存)
    • interface-adapters/: コントローラー(applicationとentitiesに依存)
  3. di/: 依存関係を接続するためのDependency Injectionコンテナとモジュール。

  4. drizzle/: データベーススキーマとマイグレーション(インフラストラクチャの関心事)。

  5. tests/: src/と同じ構造で組織化されたユニットテスト。

3.3. フロントエンドにおけるレイヤー構造

Screenshot 2025-11-24 at 11.27.42.png

私たちのプロジェクトでは、Clean Architectureは4つの主要なレイヤーに組織化されています:

┌─────────────────────────────────────────┐
│   Frameworks & Drivers Layer (app/)      │
│   - Next.js Pages, Components           │
│   - Server Actions, Route Handlers      │
│   - UI Components (Shadcn UI)           │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Interface Adapters Layer              │
│   (src/interface-adapters/)             │
│   - Controllers                         │
│   - Input Validation                    │
│   - Output Formatting (Presenters)      │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Application Layer                     │
│   (src/application/)                    │
│   - Use Cases                           │
│   - Repository Interfaces               │
│   - Service Interfaces                  │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Entities Layer                        │
│   (src/entities/)                       │
│   - Domain Models                       │
│   - Business Rules                      │
│   - Custom Errors                       │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Infrastructure Layer                  │
│   (src/infrastructure/)                 │
│   - Repository Implementations          │
│   - Service Implementations             │
│   - External API Clients (Stripe)       │
└─────────────────────────────────────────┘

3.2.1. Entities Layer (Domain Layer)

場所: src/entities/

これは最内層で、コアなビジネスエンティティビジネスルールを含みます。このレイヤーは、任意のフレームワークやライブラリから完全に独立しています。

eコマースプロジェクトの例

// src/entities/models/order.ts
import { z } from 'zod';

export const orderStatusSchema = z.enum([
  'pending',
  'processing',
  'shipped',
  'delivered',
  'cancelled',
]);

export const orderSchema = z.object({
  id: z.string(),
  userId: z.string(),
  items: z.array(orderItemSchema),
  totalAmount: z.number().positive(),
  status: orderStatusSchema,
  shippingAddress: addressSchema,
  createdAt: z.date(),
  updatedAt: z.date(),
});

export type Order = z.infer<typeof orderSchema>;
export type OrderStatus = z.infer<typeof orderStatusSchema>;

// Business rule: Order total must match sum of items
export function validateOrderTotal(order: Order): boolean {
  const calculatedTotal = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  return Math.abs(calculatedTotal - order.totalAmount) < 0.01;
}

特徴

  • プレーンなTypeScript/JavaScriptのみを含み、外部依存関係がない
  • データ構造バリデーションルールを定義
  • 上位の任意のレイヤーで使用可能

3.2.2. Application Layer

場所: src/application/

このレイヤーにはユースケース(ビジネスロジック)と、リポジトリとサービスのインターフェースが含まれます。これは「アプリケーションが何ができるか」を定義する場所です。

ユースケース

各ユースケースは、特定のビジネス操作を表します。例:

// src/application/use-cases/orders/create-order.use-case.ts
import { Order } from '@/src/entities/models/order';
import { IOrdersRepository } from '@/src/application/repositories/orders.repository.interface';
import { IPaymentService } from '@/src/application/services/payment.service.interface';
import { IInventoryService } from '@/src/application/services/inventory.service.interface';

export type ICreateOrderUseCase = ReturnType<typeof createOrderUseCase>;

export const createOrderUseCase =
  (
    ordersRepository: IOrdersRepository,
    paymentService: IPaymentService,
    inventoryService: IInventoryService
  ) =>
  async (input: {
    userId: string;
    items: Array<{ productId: string; quantity: number }>;
    shippingAddress: Address;
  }): Promise<Order> => {
    // 1. Validate inventory
    const inventoryCheck = await inventoryService.checkAvailability(
      input.items
    );
    if (!inventoryCheck.isAvailable) {
      throw new InventoryError('Some items are out of stock');
    }

    // 2. Calculate total
    const totalAmount = await calculateOrderTotal(input.items);

    // 3. Create payment intent with Stripe
    const paymentIntent = await paymentService.createPaymentIntent({
      amount: totalAmount,
      currency: 'usd',
      metadata: { userId: input.userId },
    });

    // 4. Create order
    const order = await ordersRepository.createOrder({
      userId: input.userId,
      items: input.items,
      totalAmount,
      shippingAddress: input.shippingAddress,
      paymentIntentId: paymentIntent.id,
      status: 'pending',
    });

    return order;
  };

インターフェース

このレイヤーは、外部レイヤーが実装する必要があるインターフェース(契約)を定義します:

// src/application/repositories/orders.repository.interface.ts
import { Order } from '@/src/entities/models/order';

export interface IOrdersRepository {
  createOrder(order: CreateOrderInput): Promise<Order>;
  getOrderById(orderId: string): Promise<Order | null>;
  getOrdersByUserId(userId: string): Promise<Order[]>;
  updateOrderStatus(orderId: string, status: OrderStatus): Promise<Order>;
}

// src/application/services/payment.service.interface.ts
export interface IPaymentService {
  createPaymentIntent(input: {
    amount: number;
    currency: string;
    metadata: Record<string, string>;
  }): Promise<PaymentIntent>;

  confirmPayment(paymentIntentId: string): Promise<PaymentResult>;

  refundPayment(paymentId: string, amount?: number): Promise<RefundResult>;
}

特徴

  • ユースケースは操作をオーケストレートし、詳細を実装しない
  • 具象実装ではなくインターフェースを使用
  • 依存関係を注入するためにDependency Injectionが使用される

3.2.3. Interface Adapters Layer

場所: src/interface-adapters/

このレイヤーにはコントローラー(システムのエントリーポイント)が含まれます。コントローラーには以下の責任があります:

  1. 入力バリデーション: UIからの入力をバリデートしてパース
  2. 認証/認可: アクセス権限をチェック
  3. オーケストレーション: 適切なユースケースを呼び出す
  4. 出力フォーマット: UIに返す前にデータをフォーマット

コントローラーの例

// src/interface-adapters/controllers/orders/create-order.controller.ts
import { z } from 'zod';
import { InputParseError } from '@/src/entities/errors/common';
import { ICreateOrderUseCase } from '@/src/application/use-cases/orders/create-order.use-case';
import { IInstrumentationService } from '@/src/application/services/instrumentation.service.interface';

const createOrderInputSchema = z.object({
  items: z
    .array(
      z.object({
        productId: z.string().uuid(),
        quantity: z.number().int().positive(),
      })
    )
    .min(1),
  shippingAddress: addressSchema,
});

export type ICreateOrderController = ReturnType<typeof createOrderController>;

export const createOrderController =
  (
    instrumentationService: IInstrumentationService,
    createOrderUseCase: ICreateOrderUseCase,
    getCurrentUser: () => Promise<{ id: string }>
  ) =>
  async (
    input: Partial<z.infer<typeof createOrderInputSchema>>
  ): Promise<{ order: Order; clientSecret: string }> => {
    return await instrumentationService.startSpan(
      { name: 'createOrder Controller' },
      async () => {
        // 1. Validate input
        const { data, error: parseError } =
          createOrderInputSchema.safeParse(input);
        if (parseError) {
          throw new InputParseError('Invalid order data', {
            cause: parseError,
          });
        }

        // 2. Get authenticated user
        const user = await getCurrentUser();

        // 3. Call use case
        const order = await createOrderUseCase({
          userId: user.id,
          items: data.items,
          shippingAddress: data.shippingAddress,
        });

        // 4. Format output (Presenter pattern)
        return {
          order: {
            id: order.id,
            status: order.status,
            totalAmount: order.totalAmount,
            items: order.items.map((item) => ({
              productId: item.productId,
              quantity: item.quantity,
              price: item.price,
            })),
          },
          clientSecret: order.paymentIntent.clientSecret,
        };
      }
    );
  };

特徴

  • コントローラーはビジネスロジックを含まず、オーケストレートのみ
  • フレームワーク固有の関心事(フォームデータ、クッキーなど)を処理
  • UIフォーマットとドメインフォーマット間でデータを変換

3.2.4. Frameworks & Drivers Layer

場所: app/

これは最外層で、Next.jsに関連するすべてのコードを含みます:

  • Pages/Route Handlers: app/orders/page.tsx, app/api/orders/route.ts
  • Server Actions: app/actions.ts
  • Components: Shadcn UIを使用するReactコンポーネント
  • Styles: CSS/Tailwindクラス

Server Actionの例

// app/orders/actions.ts
'use server';

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { getInjection } from '@/di/container';
import { InputParseError } from '@/src/entities/errors/common';

export async function createOrder(formData: FormData) {
  const instrumentationService = getInjection('IInstrumentationService');

  return await instrumentationService.instrumentServerAction(
    'createOrder',
    { recordResponse: true },
    async () => {
      const items = JSON.parse(formData.get('items')?.toString() || '[]');
      const shippingAddress = JSON.parse(
        formData.get('shippingAddress')?.toString() || '{}'
      );

      let result;
      try {
        const createOrderController = getInjection('ICreateOrderController');
        result = await createOrderController({
          items,
          shippingAddress,
        });
      } catch (err) {
        if (err instanceof InputParseError) {
          return { error: 'Invalid order data' };
        }
        // Handle other errors...
        return { error: 'Failed to create order' };
      }

      // Redirect to payment page
      redirect(`/orders/${result.order.id}/payment`);
    }
  );
}

コンポーネントの例

// app/orders/create/page.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createOrder } from '../actions';
import { Button } from '@/app/_components/ui/button';
import { Input } from '@/app/_components/ui/input';

export default function CreateOrderPage() {
  const form = useForm({
    resolver: zodResolver(orderFormSchema),
  });

  const onSubmit = async (data: OrderFormData) => {
    const formData = new FormData();
    formData.append('items', JSON.stringify(data.items));
    formData.append('shippingAddress', JSON.stringify(data.shippingAddress));

    const result = await createOrder(formData);
    if (result?.error) {
      // Handle error
    }
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* Form fields */}
    </form>
  );
}

特徴

  • このレイヤーはコントローラーのみを使用し、ユースケースを直接呼び出さない
  • すべてのフレームワーク固有のコードを含む
  • ビジネスロジックに影響を与えずに変更可能

3.2.5. Infrastructure Layer

場所: src/infrastructure/

このレイヤーには、Application Layerで定義されたインターフェースの実装が含まれます:

  • Repositories: データベース操作
  • Services: 外部APIクライアント(Stripe、メールサービス)
  • Adapters: ドメインモデルと外部フォーマット間の変換

リポジトリ実装の例

// src/infrastructure/repositories/orders.repository.ts
import { db } from '@/drizzle';
import { orders } from '@/drizzle/schema';
import { IOrdersRepository } from '@/src/application/repositories/orders.repository.interface';
import { Order } from '@/src/entities/models/order';
import { OrderNotFoundError } from '@/src/entities/errors/orders';

export class OrdersRepository implements IOrdersRepository {
  async createOrder(input: CreateOrderInput): Promise<Order> {
    const [order] = await db
      .insert(orders)
      .values({
        id: crypto.randomUUID(),
        userId: input.userId,
        items: JSON.stringify(input.items),
        totalAmount: input.totalAmount,
        status: 'pending',
        shippingAddress: JSON.stringify(input.shippingAddress),
        paymentIntentId: input.paymentIntentId,
        createdAt: new Date(),
        updatedAt: new Date(),
      })
      .returning();

    return this.mapToDomain(order);
  }

  async getOrderById(orderId: string): Promise<Order | null> {
    const order = await db.query.orders.findFirst({
      where: eq(orders.id, orderId),
    });

    if (!order) {
      return null;
    }

    return this.mapToDomain(order);
  }

  private mapToDomain(dbOrder: DbOrder): Order {
    return {
      id: dbOrder.id,
      userId: dbOrder.userId,
      items: JSON.parse(dbOrder.items),
      totalAmount: dbOrder.totalAmount,
      status: dbOrder.status as OrderStatus,
      shippingAddress: JSON.parse(dbOrder.shippingAddress),
      createdAt: dbOrder.createdAt,
      updatedAt: dbOrder.updatedAt,
    };
  }
}

サービス実装の例(Stripe)

// src/infrastructure/services/payment.service.ts
import Stripe from 'stripe';
import { IPaymentService } from '@/src/application/services/payment.service.interface';
import { PaymentError } from '@/src/entities/errors/payment';

export class StripePaymentService implements IPaymentService {
  private stripe: Stripe;

  constructor() {
    this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
      apiVersion: '2024-11-20.acacia',
    });
  }

  async createPaymentIntent(input: {
    amount: number;
    currency: string;
    metadata: Record<string, string>;
  }): Promise<PaymentIntent> {
    try {
      const paymentIntent = await this.stripe.paymentIntents.create({
        amount: Math.round(input.amount * 100), // Convert to cents
        currency: input.currency,
        metadata: input.metadata,
        automatic_payment_methods: {
          enabled: true,
        },
      });

      return {
        id: paymentIntent.id,
        clientSecret: paymentIntent.client_secret!,
        status: paymentIntent.status,
      };
    } catch (error) {
      throw new PaymentError('Failed to create payment intent', {
        cause: error,
      });
    }
  }

  async confirmPayment(paymentIntentId: string): Promise<PaymentResult> {
    try {
      const paymentIntent =
        await this.stripe.paymentIntents.retrieve(paymentIntentId);

      if (paymentIntent.status === 'succeeded') {
        return {
          success: true,
          paymentId: paymentIntent.id,
        };
      }

      return {
        success: false,
        error: paymentIntent.last_payment_error?.message,
      };
    } catch (error) {
      throw new PaymentError('Failed to confirm payment', { cause: error });
    }
  }
}

特徴

  • Application Layerのインターフェースを実装
  • すべての外部依存関係(データベース、API、ライブラリ)を含む
  • ユースケースに影響を与えずに実装を変更可能

3.4. Dependency Injection (DI)

レイヤーが具象実装ではなくインターフェースを使用できるようにするために、Dependency Injectionを使用します:

// di/modules/orders.module.ts
import { container } from '@/di/container';
import { OrdersRepository } from '@/src/infrastructure/repositories/orders.repository';
import { StripePaymentService } from '@/src/infrastructure/services/payment.service';
import { CreateOrderController } from '@/src/interface-adapters/controllers/orders/create-order.controller';
import { createOrderUseCase } from '@/src/application/use-cases/orders/create-order.use-case';

// Register implementations
container.bind('IOrdersRepository', () => new OrdersRepository());
container.bind('IPaymentService', () => new StripePaymentService());

// Register use cases
container.bind('ICreateOrderUseCase', () => {
  const ordersRepo = container.get('IOrdersRepository');
  const paymentService = container.get('IPaymentService');
  const inventoryService = container.get('IInventoryService');

  return createOrderUseCase(ordersRepo, paymentService, inventoryService);
});

// Register controllers
container.bind('ICreateOrderController', () => {
  const instrumentationService = container.get('IInstrumentationService');
  const createOrderUseCase = container.get('ICreateOrderUseCase');

  return createOrderController(
    instrumentationService,
    createOrderUseCase,
    getCurrentUser
  );
});

3.5. 依存関係ルール

Clean Architectureで最も重要なのは依存関係ルールです:

依存関係は外側から内側へのみ流れ、逆方向には流れない

app/ (Frameworks)
  ↓ uses
interface-adapters/ (Controllers)
  ↓ uses
application/ (Use Cases)
  ↓ uses
entities/ (Models)
  ↑ implements
infrastructure/ (Repositories, Services)

このルールを強制するために、ESLintプラグインを使用します:

// .eslintrc.json
{
  "plugins": ["boundaries"],
  "rules": {
    "boundaries/element-types": [
      2,
      {
        "default": "disallow",
        "rules": [
          {
            "from": ["app"],
            "allow": ["controllers", "models", "errors"]
          },
          {
            "from": ["src/interface-adapters"],
            "allow": ["use-cases", "models", "errors"]
          },
          {
            "from": ["src/application"],
            "allow": ["models", "errors", "repositories", "services"]
          },
          {
            "from": ["src/infrastructure"],
            "allow": ["models", "errors"]
          }
        ]
      }
    ]
  }
}

3.6. 完全なリクエスト処理フロー

「注文を作成」リクエストが各レイヤーを通じてどのように処理されるかを見てみましょう:

1. User submits form
   ↓
2. app/orders/create/page.tsx (Component)
   - Collects form data
   - Calls Server Action
   ↓
3. app/orders/actions.ts (Server Action)
   - Parses FormData
   - Gets controller from DI container
   - Calls controller
   ↓
4. src/interface-adapters/controllers/orders/create-order.controller.ts
   - Validates input với Zod
   - Checks authentication
   - Calls use case
   - Formats output
   ↓
5. src/application/use-cases/orders/create-order.use-case.ts
   - Orchestrates business logic:
     * Check inventory
     * Calculate total
     * Create payment intent
     * Create order
   - Uses repository và service interfaces
   ↓
6. src/infrastructure/repositories/orders.repository.ts
   - Executes database query
   - Maps DB format to domain model
   ↓
7. src/infrastructure/services/payment.service.ts
   - Calls Stripe API
   - Maps Stripe response to domain format
   ↓
8. Response flows back through layers
   - Controller formats data
   - Server Action handles response
   - Component updates UI

4. 長所と短所

4.1. 長所

✅ 独立性と柔軟性

  • フレームワークからの独立性: Next.jsからRemix、またはReact Nativeに移行してもビジネスロジックは変更されない
  • インフラストラクチャの変更が容易: StripeからPayPal、またはSQLiteからPostgreSQLに変更しても実装を変更するだけで済む
  • テスト可能: 各レイヤーをモックで独立してテストできる

✅ 保守性

  • 明確なコード組織: すべての開発者がコードをどこに配置すべきか知っている
  • 関心の分離: 各レイヤーに明確な責任があり、理解と保守が容易
  • スケーラビリティ: 既存のコードに影響を与えずに新機能を追加しやすい

✅ チームコラボレーション

  • 並行開発: 複数の開発者が異なるレイヤーで同時に作業できる
  • オンボーディング: 新しい開発者がプロジェクト構造を理解しやすい
  • コードレビュー: 各ファイルがどのレイヤーに属するか明確で、レビューが効率的

✅ TypeScriptによる型安全性

  • インターフェースが契約として機能: 各コンポーネントの入力/出力が明確
  • コンパイル時エラー: コードを実行する前にエラーを検出

4.2. 短所

❌ 学習曲線

  • 初期の複雑さ: 新しい開発者がアーキテクチャを理解するのに時間がかかる
  • 過度な設計: 小さなプロジェクトには複雑すぎる可能性がある
  • ボイラープレートコード: より多くのコード(インターフェース、DIセットアップなど)を書く必要がある

❌ パフォーマンスオーバーヘッド

  • 間接化: 多くのレイヤーが少し遅くなる可能性がある(通常は無視できる程度)
  • バンドルサイズ: 多くの抽象化がバンドルサイズを増やす可能性がある(最適化可能)

❌ 開発速度

  • 初期は遅い: セットアップと初期コードの記述に時間がかかる
  • 規律が必要: チームはアーキテクチャルールに従う必要があり、「ショートカット」は許されない

❌ すべてのプロジェクトに適しているわけではない

  • 小さなプロジェクト: MVPやプロトタイプには過剰な可能性がある
  • シンプルなCRUDアプリ: アプリが非常にシンプルな場合、不要な可能性がある

4.3. Clean Architectureを使用すべきタイミング

使用すべき場合

  • ✅ 多くのビジネスロジックを持つ大規模で複雑なプロジェクト
  • ✅ 大規模なチーム、良好なコラボレーションが必要
  • ✅ 長期的なプロジェクト、長期的な保守が必要
  • ✅ 多くの外部サービスとの統合が必要
  • ✅ 高いテストカバレッジが必要
  • ✅ 将来技術スタックを変更する可能性がある

使用すべきでない場合

  • ❌ 迅速にリリースする必要があるMVPやプロトタイプ
  • ❌ 小さくシンプルなプロジェクト
  • ❌ 小さなチーム、アーキテクチャパターンの経験がない
  • ❌ 締切が迫っており、セットアップの時間がない

5. コードルールとベストプラクティス

5.1. コードルールチェックリスト

📁 ファイル構造ルール

  • Entities Layer (src/entities/)

    • モデル: {entity}.ts(例: order.ts
    • エラー: {domain}.ts(例: orders.ts
    • 任意のフレームワーク/ライブラリをインポートしない
  • Application Layer (src/application/)

    • ユースケース: {action}-{entity}.use-case.ts
    • インターフェース: {name}.interface.ts
    • entities/からのみインポート
  • Infrastructure Layer (src/infrastructure/)

    • リポジトリ: {entity}.repository.ts
    • サービス: {purpose}.service.ts
    • application/のインターフェースを実装
  • Interface Adapters Layer (src/interface-adapters/)

    • コントローラー: {action}-{entity}.controller.ts
    • ユースケースのみを呼び出し、リポジトリ/サービスを直接呼び出さない

🎯 ユースケースルール

  • 1つのユースケース = 1つのビジネス操作

    // ✅ Good
    createOrderUseCase();
    cancelOrderUseCase();
    
    // ❌ Bad
    createOrderAndSendEmailUseCase(); // 2つのユースケースに分割
    
  • ユースケースは他のユースケースを呼び出さない

    // ❌ Bad: ユースケースが他のユースケースを呼び出す
    export const createOrderUseCase = async () => {
      await sendEmailUseCase(); // 許可されない!
    };
    
    // ✅ Good: コントローラーがオーケストレート
    export const createOrderController = async () => {
      const order = await createOrderUseCase();
      await sendEmailUseCase(); // コントローラーが呼び出す
    };
    
  • ユースケースは事前にバリデートされた入力を受け取る

    // ✅ Good: コントローラーで既にバリデートされた入力
    export const createOrderUseCase = (input: CreateOrderInput) => { ... };
    
    // ❌ Bad: ユースケース内でバリデート
    export const createOrderUseCase = (input: unknown) => {
      if (!input.userId) throw Error(); // 許可されない!
    };
    

🎮 コントローラールール

  • コントローラーはオーケストレートのみで、ビジネスロジックを含まない

    // ✅ Good: フローをオーケストレート
    export const createOrderController = async (input) => {
      validateInput(input);        // ✅ バリデーション
      const user = getCurrentUser(); // ✅ 認証
      const order = await createOrderUseCase(input); // ✅ ユースケースを呼び出し
      return formatOutput(order);   // ✅ フォーマット
    };
    
    // ❌ Bad: コントローラー内のビジネスロジック
    export const createOrderController = async (input) => {
      const total = input.items.reduce(...); // ❌ ビジネスロジック!
    };
    
  • 出力をフォーマットするためにプレゼンターを使用

    // ✅ Good: 返す前にフォーマット
    return {
      order: { id, status, totalAmount }, // 必要なフィールドのみ
      clientSecret: order.paymentIntent.clientSecret,
    };
    

⚠️ エラーハンドリングルール

  • 各ドメインのカスタムエラー

    // src/entities/errors/orders.ts
    export class OrderNotFoundError extends Error {}
    export class InsufficientInventoryError extends Error {}
    
  • 適切なレイヤーでのエラーハンドリング

    // ✅ Infrastructure: 外部エラーをラップ
    try {
      const dbOrder = await db.query.orders.findFirst(...);
      if (!dbOrder) throw new OrderNotFoundError(id);
    } catch (error) {
      if (error instanceof OrderNotFoundError) throw error;
      throw new DatabaseError('Failed', { cause: error });
    }
    
    // ✅ Controller: ドメインエラーを処理
    try {
      return await createOrderUseCase(input);
    } catch (error) {
      if (error instanceof InsufficientInventoryError) {
        return { error: 'Out of stock' };
      }
      throw error;
    }
    

🧪 テストルール

  • 各レイヤーを独立してテスト

    // ✅ モックでユースケースをテスト
    const mockRepo = { createOrder: jest.fn() };
    const useCase = createOrderUseCase(mockRepo);
    await useCase(input);
    expect(mockRepo.createOrder).toHaveBeenCalled();
    
  • 実装ではなくインターフェースをモック

    // ✅ Good: インターフェースをモック
    const mockRepo: IOrdersRepository = { ... };
    
    // ❌ Bad: 具象クラスをモック
    const mockRepo = new OrdersRepository();
    

📝 TypeScriptルール

  • 契約にはインターフェース、データには型

    // ✅ Good
    export interface IOrdersRepository { ... } // 契約
    export type Order = { ... }; // データ構造
    
  • 厳密な型チェックを有効化

    { "compilerOptions": { "strict": true } }
    

5.2. 一般的なフローとパターン

Flow 1: 注文の作成

// 1. Controller: バリデートと認証
export const createOrderController = async (input) => {
  const { data, error } = schema.safeParse(input);
  if (error) throw new InputParseError();

  const user = await getCurrentUser();
  return await createOrderUseCase({ ...data, userId: user.id });
};

// 2. Use Case: ビジネスロジック
export const createOrderUseCase =
  (repo, payment, inventory) => async (input) => {
    await inventory.checkAvailability(input.items);
    const total = calculateTotal(input.items);
    const paymentIntent = await payment.createPaymentIntent(total);
    return await repo.createOrder({ ...input, total, paymentIntent });
  };

// 3. Infrastructure: 外部呼び出し
export class OrdersRepository {
  async createOrder(input) {
    const [order] = await db.insert(orders).values(input).returning();
    return this.mapToDomain(order);
  }
}

Flow 2: 決済処理

// Controller → Use Case → Service → Stripe API
export const processPaymentController = async (paymentIntentId) => {
  const { data } = schema.parse({ paymentIntentId });
  return await processPaymentUseCase(data.paymentIntentId);
};

export const processPaymentUseCase =
  (paymentService) => async (paymentIntentId) => {
    const result = await paymentService.confirmPayment(paymentIntentId);
    if (result.success) {
      await updateOrderStatus(orderId, 'paid');
    }
    return result;
  };

export class StripePaymentService {
  async confirmPayment(id) {
    const intent = await this.stripe.paymentIntents.retrieve(id);
    return { success: intent.status === 'succeeded' };
  }
}

Flow 3: 注文のキャンセル

// Controller → Use Case → Repository + Service
export const cancelOrderController = async (orderId) => {
  const user = await getCurrentUser();
  return await cancelOrderUseCase(orderId, user.id);
};

export const cancelOrderUseCase =
  (repo, payment) => async (orderId, userId) => {
    const order = await repo.getOrderById(orderId);
    if (order.userId !== userId) throw new UnauthorizedError();
    if (order.status === 'shipped') throw new CannotCancelError();

    await payment.refund(order.paymentId);
    return await repo.updateStatus(orderId, 'cancelled');
  };

Flow 4: 注文リストの取得

// Simple flow: Controller → Use Case → Repository
export const getOrdersController = async () => {
  const user = await getCurrentUser();
  return await getOrdersUseCase(user.id);
};

export const getOrdersUseCase = (repo) => async (userId) => {
  const orders = await repo.getOrdersByUserId(userId);
  return orders.map(formatOrderForUI); // Presenter
};

5.3. 避けるべきアンチパターン

❌ アンチパターン1: コントローラー内のビジネスロジック

// ❌ Bad
export const createOrderController = async (input) => {
  const total = input.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  ); // ビジネスロジック!
  // ...
};

// ✅ Good: ユースケースに移動
export const createOrderUseCase = (repo) => async (input) => {
  const total = calculateOrderTotal(input.items); // ビジネスロジックはここ
  // ...
};

❌ アンチパターン2: ユースケースが他のユースケースを呼び出す

// ❌ Bad
export const createOrderUseCase = async (input) => {
  const order = await createOrder();
  await sendEmailUseCase(order); // 許可されない!
};

// ✅ Good: コントローラーがオーケストレート
export const createOrderController = async (input) => {
  const order = await createOrderUseCase(input);
  await sendEmailUseCase(order); // コントローラーが呼び出す
};

❌ アンチパターン3: 外側のレイヤーから内側のレイヤーへのインポート

// ❌ Bad: EntitiesがInfrastructureからインポート
// src/entities/models/order.ts
import { db } from '@/drizzle'; // 許可されない!

// ✅ Good: InfrastructureがEntitiesからインポート
// src/infrastructure/repositories/orders.repository.ts
import { Order } from '@/src/entities/models/order';

❌ アンチパターン4: ユースケース内のフレームワークコード

// ❌ Bad: ユースケース内のNext.js
export const createOrderUseCase = async (input) => {
  const session = await getServerSession(); // Next.js API!
  // ...
};

// ✅ Good: 依存関係を渡す
export const createOrderUseCase = (getUser) => async (input) => {
  const user = await getUser(); // 依存性注入
  // ...
};

6. AIに優しいベストプラクティス(Cursor、Claude、GitHub Copilot)

💡 重要: Clean Architectureは開発者を助けるだけでなく、AIアシスタントがコードを理解し、より効率的に作業するのにも役立ちます。明確な構造、一貫した命名規則、優れたドキュメントにより、AIはより正確なコードを提案し、より安全にリファクタリングし、アーキテクチャに適合したコードを生成できます。

6.0. AI用のファイルルール: .cursorrules

AIアシスタント(Cursor、Claude、GitHub Copilot)がプロジェクトのアーキテクチャとルールを理解できるように、プロジェクトのルートに.cursorrulesファイルを作成します:

# Clean Architecture Rules for AI Assistants

#### Architecture Overview

This project follows Clean Architecture with 4 layers:

1. Entities Layer (src/entities/) - Domain models
2. Application Layer (src/application/) - Use cases & interfaces
3. Infrastructure Layer (src/infrastructure/) - Implementations
4. Interface Adapters Layer (src/interface-adapters/) - Controllers

#### Dependency Rules

- Dependencies flow INWARD only
- NEVER import from outer layers into inner layers

#### File Naming Conventions

- Use cases: {action}-{entity}.use-case.ts
- Controllers: {action}-{entity}.controller.ts
- Repositories: {entity}.repository.ts

#### Code Rules

- Use cases: One operation, no calling other use cases
- Controllers: Only orchestrate, no business logic
- Always use interfaces, not concrete implementations

.cursorrulesの利点

  • AIがコンテキストを理解: AIは最初からアーキテクチャとルールを知ることができる
  • 正確な提案: AIが正しいレイヤーと正しいパターンでコードを提案する
  • 安全なリファクタリング: AIが依存関係を知り、ルールに違反しない
  • 一貫したコード: AIがプロジェクトの規約に従ってコードを生成する

.cursorrulesを使用したAI提案の例

// User: "製品リストを取得するユースケースを作成"

// ✅ AIが正しいパターンを提案:
// src/application/use-cases/products/get-products.use-case.ts
export const getProductsUseCase =
  (repo: IProductsRepository) =>
  async (filters: ProductFilters): Promise<Product[]> => {
    return await repo.getProducts(filters);
  };

// ❌ .cursorrulesがない場合、AIは間違った提案をする可能性がある:
// app/products/page.tsx (間違ったレイヤー!)
export async function getProducts() {
  const products = await db.query.products.findMany(); // UI内のビジネスロジック!
}

Cursor、Claude、GitHub CopilotなどのAIコーディングアシスタントの発展により、AIに優しいコードを書くことが重要になっています。Clean Architectureは非常に役立ちますが、さらに改善できます:

6.1. コードドキュメントとコメント

なぜAIにとって重要か?: AIはコメントとJSDocを読んでコードのコンテキストと目的を理解します。良いコメントは、AIがより適切なコードを提案するのに役立ちます。

✅ 関数とインターフェースのJSDocコメント

/**
 * 指定されたアイテムと配送先住所で新しい注文を作成します。
 *
 * @param input - アイテムと配送先住所を含む注文作成入力
 * @returns 決済インテントを含む作成された注文を解決するPromise
 * @throws {InsufficientInventoryError} 一部のアイテムが在庫切れの場合
 * @throws {PaymentError} 決済インテントの作成に失敗した場合
 *
 * @example
 * ```typescript
 * const order = await createOrderUseCase({
 *   userId: 'user-123',
 *   items: [{ productId: 'prod-1', quantity: 2 }],
 *   shippingAddress: { street: '123 Main St', city: 'NYC' }
 * });
 * ```
 */
export const createOrderUseCase =
  (
    ordersRepository: IOrdersRepository,
    paymentService: IPaymentService,
    inventoryService: IInventoryService
  ) =>
  async (input: CreateOrderInput): Promise<Order> => {
    // Implementation...
  };

✅ 「なぜ」を説明するインラインコメント、「何」ではなく

// ✅ Good: 理由を説明
// 利用できないアイテムに対して顧客に請求することを避けるため、
// 決済インテントを作成する前に在庫をチェックします
const inventoryCheck = await inventoryService.checkAvailability(input.items);
if (!inventoryCheck.isAvailable) {
  throw new InsufficientInventoryError();
}

// ❌ Bad: 自明なことを説明
// 在庫が利用可能かどうかをチェック
const inventoryCheck = await inventoryService.checkAvailability(input.items);

6.2. 明確な命名規則

なぜAIにとって重要か?: AIは命名パターンを使用してコード構造を理解します。一貫した命名により、AIはより正確にコードを予測し、提案できます。

✅ AIがコンテキストを理解しやすい説明的な名前

// ✅ Good: 明確な名前、AIが理解しやすい
export const createOrderWithPaymentIntentUseCase = ...
export const validateOrderItemsAvailability = ...
export const calculateOrderTotalWithTax = ...

// ❌ Bad: 短い名前、AIが理解しにくい
export const createOrder = ... // 決済があるかどうか不明?
export const validate = ... // 何をバリデート?
export const calc = ... // 何を計算?

✅ 一貫した命名パターン

// ✅ Good: 一貫したパターン
// ユースケース: {action}{Entity}UseCase
createOrderUseCase;
cancelOrderUseCase;
updateOrderStatusUseCase;

// コントローラー: {action}{Entity}Controller
createOrderController;
cancelOrderController;

// リポジトリ: {entity}Repository
ordersRepository;
productsRepository;

6.3. 詳細な型定義

なぜAIにとって重要か?: TypeScriptの型はAIにとって「ドキュメント」です。明確な型により、AIはデータ構造を理解し、型安全なコードを提案できます。

anyunknownではなく明示的な型

// ✅ Good: 明確な型
export interface CreateOrderInput {
  userId: string;
  items: Array<{
    productId: string;
    quantity: number;
    price: number;
  }>;
  shippingAddress: {
    street: string;
    city: string;
    zipCode: string;
    country: string;
  };
}

// ❌ Bad: anyを使用
export const createOrderUseCase = (input: any) => { ... };

✅ 複雑な型の型エイリアス

// ✅ Good: 型エイリアスによりコードが読みやすくなる
type OrderItem = {
  productId: string;
  quantity: number;
  price: number;
};

type ShippingAddress = {
  street: string;
  city: string;
  zipCode: string;
  country: string;
};

export interface CreateOrderInput {
  userId: string;
  items: OrderItem[];
  shippingAddress: ShippingAddress;
}

6.4. コード構造と組織化

なぜAIにとって重要か?: Clean Architectureは非常に役立ちます!AIはファイル構造に基づいてコード組織を理解します。明確な構造 = より良いAI提案。

✅ 小さく、焦点を絞った関数

// ✅ Good: 小さな関数、AIが理解しやすく提案しやすい
export const createOrderUseCase = (...) => async (input) => {
  await validateInventory(input.items);
  const total = await calculateOrderTotal(input.items);
  const paymentIntent = await createPaymentIntent(total);
  const order = await saveOrder({ ...input, total, paymentIntent });
  return order;
};

// ❌ Bad: 大きな関数、AIが追いにくい
export const createOrderUseCase = (...) => async (input) => {
  // 100行のコード...
};

✅ 早期リターンとガード句

// ✅ Good: 早期リターン、ロジックが明確
export const processPaymentUseCase = async (paymentIntentId: string) => {
  if (!paymentIntentId) {
    throw new InvalidInputError('Payment intent ID is required');
  }

  const paymentIntent = await paymentService.retrieve(paymentIntentId);
  if (!paymentIntent) {
    throw new PaymentNotFoundError(paymentIntentId);
  }

  if (paymentIntent.status === 'succeeded') {
    return { success: true, paymentId: paymentIntent.id };
  }

  // Continue processing...
};

// ❌ Bad: ネストされたif、読みにくい
export const processPaymentUseCase = async (paymentIntentId: string) => {
  if (paymentIntentId) {
    const paymentIntent = await paymentService.retrieve(paymentIntentId);
    if (paymentIntent) {
      if (paymentIntent.status === 'succeeded') {
        return { success: true };
      } else {
        // ...
      }
    }
  }
};

6.5. コンテキストと例

なぜAIにとって重要か?: AIは例から学習します。コードベースに例があると、AIは既存のパターンに従ってコードを提案できます。

✅ READMEとドキュメントファイル

docs/ARCHITECTURE.mdファイルを作成してアーキテクチャを明確に説明:

# Architecture Overview

## Layers

### Entities Layer

Contains domain models and business rules.
Location: `src/entities/`

### Application Layer

Contains use cases and interfaces.
Location: `src/application/`

### Infrastructure Layer

Contains implementations of repositories and services.
Location: `src/infrastructure/`

### Interface Adapters Layer

Contains controllers that handle input/output.
Location: `src/interface-adapters/`

## Dependency Flow

[Diagram showing dependency direction]

✅ 一般的なパターンの例ファイル

// docs/examples/create-order-flow.example.ts
/**
 * 例: 注文を作成する完全なフロー
 *
 * 1. ユーザーがフォームを送信 → コンポーネント
 * 2. コンポーネントがServer Actionを呼び出し
 * 3. Server Actionがコントローラーを呼び出し
 * 4. コントローラーがバリデートしてユースケースを呼び出し
 * 5. ユースケースがビジネスロジックをオーケストレート
 * 6. インフラストラクチャレイヤーが外部呼び出しを処理
 */

// Example code...

6.6. AIに優しいコードパターン

なぜAIにとって重要か?: 純粋関数、明示的なエラーハンドリングにより、AIは動作を予測しやすくなります。予測可能なコード = より良いAI提案。

✅ 可能な限り純粋関数

// ✅ Good: 純粋関数、AIが理解しやすくテストしやすい
export function calculateOrderTotal(items: OrderItem[]): number {
  return items.reduce((total, item) => total + item.price * item.quantity, 0);
}

// ❌ Bad: 副作用、予測が困難
let total = 0;
items.forEach((item) => {
  total += item.price * item.quantity; // 副作用
});

✅ 明示的なエラーハンドリング

// ✅ Good: 明示的なエラー型
try {
  const order = await createOrderUseCase(input);
  return { success: true, order };
} catch (error) {
  if (error instanceof InsufficientInventoryError) {
    return { success: false, error: 'Out of stock' };
  }
  if (error instanceof PaymentError) {
    return { success: false, error: 'Payment failed' };
  }
  throw error; // 未知のエラーを再スロー
}

// ❌ Bad: 汎用的なエラーハンドリング
try {
  const order = await createOrderUseCase(input);
} catch (error) {
  return { error: 'Something went wrong' }; // AIはエラーが何かわからない
}

6.7. ツールと自動化

なぜAIにとって重要か?: ESLintルールはAIも従う必要があるパターンを強制します。一貫したコード = より一貫したAI提案。

6.8. まとめ: AIに優しいコードのチェックリスト

AIに優しいコードにするため、以下を確認してください:

  • .cursorrulesファイルをプロジェクトルートにアーキテクチャルールと共に配置
  • すべてのパブリック関数とインターフェースにJSDocコメント
  • 定義されたパターンに従った一貫した命名規則
  • 明示的な型 - anyを使用せず、常に明確な型
  • 小さな関数 - 各関数が1つのことを行い、AIが理解しやすい
  • コードベース内の例 - AIは例から学習
  • 可能な限り純粋関数 - 予測可能な動作
  • 明示的なエラーハンドリング - AIがエラー型を知る
  • ドキュメントファイル - ARCHITECTURE.mdCONTRIBUTING.md
  • ESLintルールでAIも従うパターンを強制

結果: このチェックリストにより、AIアシスタントは:

  • ✅ 正しいレイヤーと正しいパターンでコードを提案
  • ✅ 安全にリファクタリングし、依存関係ルールに違反しない
  • ✅ 現在のコードベースと一貫したコードを生成
  • ✅ コンテキストを理解し、適切なソリューションを提案

✅ パターンを強制するESLintルール

// .eslintrc.json
{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/explicit-function-return-type": "warn",
    "boundaries/element-types": "error" // アーキテクチャを強制
  }
}

✅ リンティング付きのPre-commitフック

// package.json
{
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint .",
    "type-check": "tsc --noEmit"
  }
}

7. 結論

フロントエンドにおけるClean Architectureは、すべての問題を解決する「銀の弾丸」ではありませんが、複雑で保守しやすく拡張可能なフロントエンドアプリケーションを構築するための強力なツールです。

主なポイントのまとめ:

  1. 関心の分離: 各レイヤーに明確な責任があり、コードを理解しやすく保守しやすくする

  2. 依存関係の逆転: インターフェースとDependency Injectionの使用により、コードが柔軟でテストしやすくなる

  3. テスト可能性: 各レイヤーを独立してテストでき、コード品質を保証する

  4. スケーラビリティ: 既存のコードに影響を与えずに新機能を追加しやすい

  5. チームコラボレーション: 明確なコード組織により、チームがより効率的に作業できる

  6. AIに優しい: 優れたドキュメントと明確なコード構造により、AIアシスタントがより効果的に支援できる

使用すべきタイミング:

  • ✅ 大規模で複雑なプロジェクト
  • ✅ 大規模なチーム、コラボレーションが必要
  • ✅ 長期的なプロジェクト
  • ✅ 高いテストカバレッジが必要
  • ✅ 将来技術スタックを変更する可能性がある

使用すべきでないタイミング:

  • ❌ MVPやプロトタイプ
  • ❌ 小さくシンプルなプロジェクト
  • ❌ 締切が迫っている
  • ❌ チームに経験がない

参考文献:

プロジェクトリポジトリの取得

このアーキテクチャの完全な実装を探索したい場合は、コメント欄にGitHubのユーザー名を残してください。リポジトリのリンクをお送りしますので、コードベースを研究したり、貢献したり、自分のプロジェクトの参考にしたりできます。

5
4
1

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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?