Interface層
HTTPリクエストを受け取りレスポンスを返す層です。弊社ではHonoをメインフレームワークとし、リソース操作にGraphQL(Pothos+Yoga)、非リソース操作にRESTを併用します。
GraphQLとRESTの使い分け
リソースのCRUD操作やリレーションを含むデータ取得にはGraphQLを使います。ログイン、OAuthコールバック、ファイルアップロードなどリソース操作ではない処理にはRESTを使います。
// リソース操作 → GraphQL
builder.queryField('posts', (t) =>
t.prismaField({
type: ['Post'],
resolve: async (query, root, args, ctx) => {
return ctx.prisma.post.findMany({ ...query })
}
})
)
// 非リソース操作 → REST
app.post('/auth/login', zValidator('json', loginSchema), async (c) => {
const { email, password } = c.req.valid('json')
const token = await authService.authenticate(email, password)
return c.json({ token })
})
ORMの選定基準
GraphQLを使用する場合はPrismaを選択します。PothosとPrismaの連携によりN+1問題が自動的に解決され、DataLoaderの実装が不要になります。REST APIのみの場合はDrizzleも選択可能です。
// PothosのqueryオブジェクトがN+1を自動解決
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
author: t.relation('author'), // N+1問題なし
comments: t.relation('comments') // N+1問題なし
})
})
Interface層の責務
この層ではバリデーション、認証、エラーのユーザー向け変換を行い、ビジネスロジックはApplication層に委譲します。HonoのContextをServiceのコンストラクタに渡します。
Application層から返されるカスタムエラーをinstanceofで判定し、ユーザーに見せるメッセージとHTTPステータスコードを決定します。内部的なエラー詳細はユーザーに露出させません。
import { HTTPException } from 'hono/http-exception'
import { PaymentFailedError } from '@/lib/errors'
app.post('/payments',
zValidator('json', paymentSchema),
async (c) => {
const data = c.req.valid('json')
const service = new PaymentService(c)
const result = await service.execute(data)
if (result instanceof PaymentFailedError) {
throw new HTTPException(400, {
message: 'お支払いに失敗しました'
})
}
if (result instanceof Error) {
throw new HTTPException(500, {
message: 'エラーが発生しました'
})
}
return c.json(result, 201)
}
)
HTTPExceptionをthrowすると、HonoのonErrorハンドラで一括してJSONレスポンスに変換できます。
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ message: err.message }, err.status)
}
return c.json({ message: 'エラーが発生しました' }, 500)
})