🔥

Zod OpenAPI Hono と Hono OpenAPI を比較してみた

に公開

はじめに

Honoを使っている際に「Zod OpenAPI Hono」と「Hono OpenAPI」の区別がつかず混乱してしまったため、自分用に比較してみました!
概要や具体的なコードも載せていますので、読み飛ばしたい方は「まとめ 」を読んでいただければと思います!
間違い等ありましたらコメントいただけると幸いです。

Zod OpenAPI

ドキュメント:https://hono.dev/examples/zod-openapi
リポジトリ:https://github.com/honojs/middleware/tree/main/packages/zod-openapi

概要

Zod OpenAPI Honoは、HonoにZodを組み合わせて値や型のバリデーションとOpenAPIドキュメント生成を同時に行えるようにしたHonoの拡張クラスです。
APIのルートを定義すると、そのまま型安全なチェック Swagger仕様書の自動生成が可能になります。(READMEから翻訳・抜粋)

使い方

  1. Zod でスキーマを定義

    import { z } from '@hono/zod-openapi'
    
    const UserSchema = z.object({
      id: z.string().openapi({ example: '123' }),
      name: z.string().openapi({ example: 'John Doe' }),
      age: z.number().openapi({ example: 42 }),
    }).openapi('User')
    
    • .openapi()でメタ情報を付与しています

  1. ルートを定義 (createRoute)

    import { createRoute } from '@hono/zod-openapi'
    
    const route = createRoute({
      method: 'get',
      path: '/users/{id}',
      responses: {
        200: {
          content: { 'application/json': { schema: UserSchema } },
          description: 'Retrieve the user',
        },
      },
    })
    
    • content: { 'application/json': { schema: UserSchema } }
      この一文がOpen API生成とバリデーションの両方を担っていますね!
    • Zodスキーマが唯一の情報源になっているようです

  1. アプリに組み込み (OpenAPIHono)

    import { OpenAPIHono } from '@hono/zod-openapi'
    
    const app = new OpenAPIHono()
    app.openapi(route, (c) => c.json({ id: '1', name: 'Ultra-man', age: 20 }))
    app.doc('/doc', { openapi: '3.0.0', info: { title: 'My API', version: '1.0.0' } })
    
    • 処理はapp.openapi(route, handler)に直接 handler を渡す形になります

Hono OpenAPI

ドキュメント:https://hono.dev/examples/hono-openapi
リポジトリ:https://github.com/rhinobase/hono-openapi

概要

hono-openapi は、Zod、Valibot、ArkType、TypeBoxといったバリデーションライブラリと統合することで、Hono APIのOpenAPIドキュメントを自動生成できるミドルウェアです。(ドキュメントから翻訳・抜粋)

使い方

  1. スキーマを定義(例: Valibot)

    import * as v from 'valibot'
    
    const querySchema = v.object({ name: v.optional(v.string()) })
    const responseSchema = v.string()
    

  1. ルートを定義 (describeRoute)

    import { Hono } from 'hono'
    import { describeRoute, resolver, validator } from 'hono-openapi'
    
    const app = new Hono()
    
    app.get(
      '/',
      describeRoute({
        description: 'Say hello to the user',
        responses: {
          200: { content: { 'text/plain': { schema: resolver(responseSchema) } } },
        },
      }),
      validator('query', querySchema),
      (c) => c.text(`Hello ${c.req.valid('query')?.name ?? 'Hono'}!`)
    )
    
    • resolver(responseSchema)でOpen APIに変換しています
    • validator('query', querySchema)でバリデーションを行っています
      → ドキュメント作成とバリデーションを別々に書くので、吐き出されたOpen APIと実装が食い違う余地がありそうです
    • 処理はdescribeRoute({ ... })でOpenAPI ドキュメントの仕様を定義する形になります

  1. OpenAPI ドキュメントを公開

    import { openAPIRouteHandler } from 'hono-openapi'
    
    app.get('/openapi', openAPIRouteHandler(app, {
      documentation: {
        info: { title: 'Hono API', version: '1.0.0' },
        servers: [{ url: 'http://localhost:3000', description: 'Local Server' }],
      },
    }))
    
    • app.get(..., handler)というように通常のHonoと同じ形式で処理を書きます

まとめ

項目 Zod OpenAPI Hono Hono OpenAPI
サポートライブラリ Zod専用 Zod / Valibot / ArkType / TypeBox など複数対応
スキーマの役割 1つのスキーマで バリデーション+ドキュメント両方 を担う 実装用とドキュメント用に 同じスキーマを両方渡す必要あり(共通化すればズレない)
ルート定義 createRoute で定義 app.get(path, describeRoute(...), validator(...), handler) で定義
処理の書き方 app.openapi(route, handler)handler を直接渡す app.get(..., handler) という 通常の Hono と同じ形式

「Zod OpenAPI Hono」はZod専用でシンプルに完結するのが特徴で、型定義からバリデーション・ドキュメント生成までを一元管理でき、よりスキーマ駆動な開発体験ができるのかなと感じています。
 対して「Hono OpenAPI」は複数のスキーマライブラリに対応している反面、resolverとvalidatorの両方にスキーマを渡す必要があり、ほんの少し冗長に感じられるかもしれません。(逆に何かしらの目的で異なるスキーマを渡せるという意味でもある)

棲み分けとしては、

  • Zodを前提にスキーマを単一ソースとして扱いたい場合には「Zod OpenAPI」
  • Zod含め、それ以外のバリデーションライブラリを使用したい場合や、通常のHonoの書き心地で実装をしたい場合には「Hono OpenAPI」

というようになるのかなと思います!

Discussion