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の実装を切り替えることで、開発・テスト・本番環境で異なる実装を使用できます。