🐟

OrvalでOpenAPI定義からコードを自動生成しスキーマ駆動でHonoアプリを開発する

2025/03/16に公開

OrvalOpenAPI仕様をもとにTypeScriptのクライアントやサーバーコード、mswモック、zodスキーマを自動生成するツールです。
Hono周辺の機能追加および改善を進めておりv7.6.0では実用可能な段階に成長しました。
本記事ではOrvalを使ってHonoサーバーのソースコードを自動生成し、APIエンドポイントを実装する方法について解説します。
なお今回使用するサンプルアプリはリポジトリ内にサンプルアプリとしてコミットしています。

Orvalの設定ファイル

以下の様にorval.config.tsを定義します。

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  api: {
    input: {
      target: './petstore.yaml',
    },
    output: {
      mode: 'tags-split',
      client: 'hono',
      target: 'src/endpoints',
      schemas: 'src/schemas',
      override: {
        hono: {
          compositeRoute: 'src/routes.ts',
          validatorOutputPath: 'src/endpoints/validator.ts',
        },
      },
    },
  },
});

自動生成されるファイルの解説

orvalコマンドで自動生成されるディレクトリ構成は以下のようになります。

src/  
├── endpoints  
│   ├── pets  
│   │   ├── pets.context.ts  
│   │   ├── pets.handlers.ts  
│   │   └── pets.zod.ts
│   └── validator.ts  
├── routes.ts  
└── schemas  
    ├── pet.ts  
    └── pets.ts  

schemas

OpenAPIに定義したリクエスト・レスポンスのスキーマ定義です。

endpoints/pets

設定ファイルでmode: tags-splitを指定しているためOpenAPItags定義毎にHonohandlerをディレクトリ分割して生成します。
handlerの実装として以下の3つのファイルを生成します。

  • pets.context.ts
  • pets.handlers.ts
  • pets.zod.ts

自動生成する度に全てのファイルを再生成してしまうと実装したコードが削除されてしまう為orvalではhandler.ts既に存在する場合は上書きしません。
context.tszod.tsのみを最新のOpenAPIの定義に合わせて再生成することにより実装と分離しながらスキーマとの整合性を担保します。

pets.context.ts

リクエストのパラメータをhandlerで扱うためにHonoContextOpenAPIから自動生成しています。

https://github.com/orval-labs/orval/blob/master/samples/hono/composite-routes-with-tags-split/src/endpoints/pets/pets.context.ts

pets.zod.ts

リクエストのパラメータ、レスポンスをバリデーションするためのzod schemaOpenAPIから自動生成しています。

https://github.com/orval-labs/orval/blob/master/samples/hono/composite-routes-with-tags-split/src/endpoints/pets/pets.zod.ts

pets.handlers.ts

前述したContextzod schemaを参照してHonohandlerを定義しています。
例えば、pets.handlers.ts は以下のようになっています。

pets.handlers.ts
import { createFactory } from 'hono/factory';  
import { zValidator } from '../../validators/pets.validator';  
import { ListPetsContext } from './pets.context';  
import { listPetsQueryParams, listPetsResponse } from './pets.zod';  

const factory = createFactory();  

export const listPetsHandlers = factory.createHandlers(  
  zValidator('query', listPetsQueryParams),  
  zValidator('response', listPetsResponse),  
  async (c: ListPetsContext) => {}
);

handlerには、バリデーションやコンテキスト定義は含まれていますが実際の処理は未実装 です。
そのため、以下の様にレスポンスを返す処理を追加します。

pets.handlers.ts
import { createFactory } from 'hono/factory';  
import { zValidator } from '../../validators/pets.validator';  
import { ListPetsContext } from './pets.context';  
import { listPetsQueryParams, listPetsResponse } from './pets.zod';  

const factory = createFactory();  

export const listPetsHandlers = factory.createHandlers(  
  zValidator('query', listPetsQueryParams),  
  zValidator('response', listPetsResponse),  
- async (c: ListPetsContext) => {}  
+ async (c: ListPetsContext) => {  
+   return c.json([  
+     {  
+       id: 1,  
+       name: 'doggie'  
+     }  
+   ]);  
+ }  
);

endpoints/validator.ts

スキーマと一致しないリクエスト、レスポンスの場合にエラーを返却する共通のバリデーションロジックです。

https://github.com/orval-labs/orval/blob/master/samples/hono/composite-routes-with-tags-split/src/endpoints/validator.ts

設定ファイルでoverride.hono.validatorOutputPathを指定する事でファイルの出力先を指定可能です。
全てのendpointsで参照する共通処理の為src/endpointsに出力する様に以下の様に定義しています。

orval.config.ts
override: {
  hono: {
    validatorOutputPath: 'src/endpoints/validator.ts'
  }
}

doc: https://orval.dev/reference/configuration/output#validatoroutputpath

routes.ts

src/endpointsに出力されたHonohandlerとURLのパスを紐付けたHonoインスタンスです。

routes.ts
import { Hono } from 'hono';  
import {  
  listPetsHandlers,  
  createPetsHandlers,  
  updatePetsHandlers,  
  showPetByIdHandlers  
} from './endpoints/pets/pets.handlers';  

const app = new Hono();  

app.get('/pets', ...listPetsHandlers);  
app.post('/pets', ...createPetsHandlers);  
app.put('/pets', ...updatePetsHandlers);  
app.get('/pets/:petId', ...showPetByIdHandlers);  

export default app;

設定ファイルでoverride.hono.compositeRouteを指定する事でファイルの出力先を指定可能です。

orval.config.ts
override: {
  hono: {
    compositeRoute: 'src/routes.ts'
  }
}

doc: https://orval.dev/reference/configuration/output#compositeroute

このファイルを出力することで常にOpenAPIに定義しているpathHonoサーバーに定義しているルーティングを一致させることが可能です。

使い方

Honoサーバーapp.tsを用意します。

app.ts
import { Hono } from 'hono';  
import routes from './routes';  

const app = new Hono();

export default app;

ここに自動生成されたroutes.tsを統合します。

app.ts
import { Hono } from 'hono';  
+ import routes from './routes';  

const app = new Hono();

+ app.route('/', routes);

export default app;

app.route('/', routes);とする事でルーティングの定義を組み込むことができるので自動生成による影響を受けません。
devサーバーを起動して動作確認ができます。

yarn wrangler dev src/app.ts
curl http://localhost:8787/pets  
#=> [{"id":1,"name":"doggie"}]

まとめ

mode: tags-splitの対応やcompositeRoutevalidatePathオプションの追加により実用可能な段階に成長しました。
OpenAPI仕様をもとにHonoのAPIエンドポイントを自動生成し、型安全な実装を効率的に行うことで堅牢なスキーマ駆動が実現可能です。

参考情報

Discussion