Interactive

Infrastructure層

外部サービスやデータベースへの接続を抽象化することで、ClaudeCodeは環境に依存しない実装を生成できます。弊社ではAdapterパターンで外部依存を隔離し、テスト可能性と移植性を確保します。

弊社の推奨ルール

  • Adapterパターンの徹底 - 外部サービスは必ずAdapterクラスで包む
  • 環境変数の型安全 - Envインターフェースで環境変数を型定義
  • リポジトリパターン - データアクセスロジックをRepositoryに集約
  • エラーはそのまま返す - カスタムエラーは作らずApplication層で変換

ClaudeCodeでの利用

データベースアダプターの作成

PrismaやDrizzleのラッパーを作成する場合

PrismaのAdapterクラスを作成して。トランザクション処理も含める

外部APIアダプターの実装

外部サービスとの連携が必要な場合

Stripe決済のAdapterを作成。エラーハンドリングとリトライ処理付き

メール送信サービスの実装

SendGridやResendなどのメール配信サービスを使う場合

Resendを使ったEmailAdapterを実装。テンプレート機能も含める

ストレージアダプターの作成

S3やR2などのオブジェクトストレージを扱う場合

Cloudflare R2のStorageAdapterを作成。署名付きURLの生成も含む

Adapterパターンでの外部サービス隔離

弊社では、すべての外部サービスをAdapterクラスで包み、ビジネスロジックから技術的詳細を隠蔽します。

外部APIをAdapterでラップ

外部サービスのAPIをAdapterクラスで包み、ビジネスロジックに必要なメソッドだけを公開します。

// Stripe決済のAdapter
export class StripeAdapter {
  constructor(props: { apiKey: string }) {
    this.stripe = new Stripe(props.apiKey)
  }

  // ビジネスロジックが必要とする簡潔なメソッド
  async createPayment(props: {
    amount: number
    customerId: string
  }) {
    // 外部APIの詳細を隠蔽
    const intent = await this.stripe.paymentIntents.create({
      amount: props.amount,
      currency: 'jpy',
      customer: props.customerId
    })
    // シンプルな結果を返す
    return { id: intent.id, secret: intent.client_secret }
  }
}
// Resendメール送信のAdapter
export class EmailAdapter {
  constructor(props: { apiKey: string }) {
    this.resend = new Resend(props.apiKey)
  }

  async send(props: { to: string; subject: string; html: string }) {
    // 外部APIの複雑な仕様を隠蔽
    const result = await this.resend.emails.send({
      from: 'noreply@example.com',
      to: props.to,
      subject: props.subject,
      html: props.html
    })
    // エラーはそのまま返す(Application層で処理)
    if (result.error) throw result.error
    return { id: result.data.id }
  }
}

Repositoryパターンでデータアクセス

Repositoryは集約ルートごとに作成し、Entityを受け取りEntityを返します。複雑な検索メソッドは定義せず、集約の不変条件を守るために必要なデータをJOINして取得します。

Prismaを使用したRepository

GraphQLで使用する場合のPrismaベースのRepositoryパターンです。集約ルートごとにRepositoryを作成します。

// 課金用Entity(フラットな構造)
export class UserBillingEntity {
  constructor(props: {
    id: string
    email: string
    stripeCustomerId: string
    subscriptionPlan: string  // JOINしたデータをフラットに
    subscriptionExpiresAt: Date
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }

  canUpgrade(): boolean {
    return this.subscriptionPlan === 'free'
  }
}

// Repository
export class UserBillingRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findOne(where: { id?: string }): Promise<UserBillingEntity | null> {
    const data = await this.prisma.user.findFirst({
      where,
      include: { subscription: true }
    })
    if (!data) return null

    return new UserBillingEntity({
      id: data.id,
      email: data.email,
      stripeCustomerId: data.stripeCustomerId,
      subscriptionPlan: data.subscription.plan,
      subscriptionExpiresAt: data.subscription.expiresAt
    })
  }

  async write(entity: UserBillingEntity): Promise<void> {
    await this.prisma.user.upsert({
      where: { id: entity.id },
      create: entity,
      update: entity
    })
  }

  async delete(id: string): Promise<void> {
    await this.prisma.user.delete({ where: { id } })
  }
}

Drizzleを使用したRepository

REST APIで使用する場合のDrizzleベースのRepositoryパターンです。

import { drizzle } from 'drizzle-orm/d1'
import { eq } from 'drizzle-orm'
import { users, profiles } from './schema'

// プロフィール用Entity(フラットな構造)
export class UserProfileEntity {
  constructor(props: {
    id: string
    name: string
    bio: string        // JOINしたデータをフラットに
    avatar: string
  }) {
    Object.assign(this, props)
    Object.freeze(this)
  }
}

// Repository
export class UserProfileRepository {
  constructor(private readonly db: ReturnType<typeof drizzle>) {}

  async findOne(where: { id?: string }): Promise<UserProfileEntity | null> {
    const result = await this.db
      .select()
      .from(users)
      .leftJoin(profiles, eq(users.id, profiles.userId))
      .where(eq(users.id, where.id))
      .limit(1)

    const data = result[0]
    if (!data) return null

    return new UserProfileEntity({
      id: data.users.id,
      name: data.users.name,
      bio: data.profiles?.bio || '',
      avatar: data.profiles?.avatar || ''
    })
  }

  async write(entity: UserProfileEntity): Promise<void> {
    await this.db.insert(users)
      .values(entity)
      .onConflictDoUpdate({ target: users.id, set: entity })
  }

  async delete(id: string): Promise<void> {
    await this.db.delete(users).where(eq(users.id, id))
  }
}

Repositoryの使用原則

// ❌ 禁止: 複雑な検索メソッドを定義しない
export class UserRepository {
  // このようなメソッドは作らない
  async findActiveUsersWithSubscription() { /* ... */ }
  async searchByNameAndEmail(name: string, email: string) { /* ... */ }
  async findUsersWithPendingPayments() { /* ... */ }
}

// ✅ 正しい: 4つの基本メソッドのみ
export class UserAuthRepository {
  async findOne(where: {}): Promise<UserAuthEntity | null> { /* ... */ }
  async findMany(where: {}): Promise<UserAuthEntity[]> { /* ... */ }
  async write(entity: UserAuthEntity): Promise<void> { /* ... */ }
  async delete(id: string): Promise<void> { /* ... */ }
}

// ✅ 正しい: 目的に応じた複数の集約ルート
const userAuthRepo = new UserAuthRepository()      // 認証用
const userProfileRepo = new UserProfileRepository() // プロフィール用
const userBillingRepo = new UserBillingRepository() // 課金用

よくある問題

外部サービスのエラー処理

Infrastructure層ではエラーをそのまま返し、Application層でユーザーフレンドリーなメッセージに変換します。これにより、エラー処理のロジックが一箇所に集約されます。

テストでの外部依存

Adapterクラスをそのままモックし、テスト時は代替実装を注入します。これにより、外部サービスなしでテストが可能になります。

Adapterの使い分け

Adapterの実装を切り替えることで、開発・テスト・本番環境で異なる実装を使用できます。