Interactive

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)
})