diff --git a/src/content/blog-metas/2024-12-15-hono-typia-openapi.json b/src/content/blog-metas/2024-12-15-hono-typia-openapi.json new file mode 100644 index 0000000..0c08893 --- /dev/null +++ b/src/content/blog-metas/2024-12-15-hono-typia-openapi.json @@ -0,0 +1,3 @@ +{ + "postDate": "2024-12-15T14:30:40.899Z" +} diff --git a/src/content/blogs/2024-12-15-hono-typia-openapi.md b/src/content/blogs/2024-12-15-hono-typia-openapi.md new file mode 100644 index 0000000..80ce492 --- /dev/null +++ b/src/content/blogs/2024-12-15-hono-typia-openapi.md @@ -0,0 +1,267 @@ +--- +title: Hono + Typia で OpenAPI ドキュメントを生成する +description: Hono + Typia で作成した Hono の型から OpenAPI ドキュメントを生成するライブラリを作りました。 +category: tech +author: miyaji +tags: [advent-calendar, javascript, typescript, openapi, hono, typia] +--- + +この記事は、[OUCC Advent Calendar 2024](https://adventar.org/calendars/10655) の 15 日目の記事です。昨日は watamario さんの [AtCoder Beginners Selection の Shift only を x86 の bsf 命令で解く](/blog/articles/2024-12-14-bsf/) でした。本日は、私が作成したHono + Typia で作成した Hono の型から OpenAPI ドキュメントを生成するライブラリについて説明します。 + +作成したライブラリはこちらです。 + +https://github.com/miyaji255/hono-typia-openapi + +## 動機 + +Hono には [@honojs/zod-openapi](https://hono.dev/examples/zod-openapi) というライブラリがあり、これを利用することでOpenAPIドキュメントを生成することができます。 + +しかし、このライブラリはその名の通りZodにしか対応しておらず、書き方もHonoから大きく変えることになり使いづらいです。TypiaはZodよりも高速なので[^1]、できることならばTypiaを使いたいところです。そこで、Honoの持つSchemaの型からOpenAPIドキュメントを生成するライブラリを作成しました。 + +また、型から生成することにより完全なゼロランタイムでOpenAPIドキュメントを生成することができます。 + +ちなみに、同じように @honojs/zod-openapi が使いづらいということで [Hono OpenAPI](https://github.com/rhinobase/hono-openapi) というライブラリも作成されています。これは Zod の他にも Valibot, Ark, TypeBox に対応していますが、Typia には対応していません。 + +## 使い方 + +CLIとPluginの2つの使い方がありますが、基本的にPluginで使うことを想定しています。 + +### インストール + +```bash +npm install hono-typia-openapi +``` + +### Plugin + +unpluginを使用して作成しているのでunpluginがサポートするフレームワーク[^2]であれば利用することができます。ここではesbuildを使った簡単な例を示します。 + +```typescript +import { build } from 'esbuild'; +import HonoTypiaOpenAPIPLugin from 'hono-typia-openapi/esbuild'; + +await build({ + entryPoints: ['src/index.ts'], + bundle: true, + outfile: 'dist/index.js', + plugins: [ + HonoTypiaOpenAPIPLugin({ + title: "My App", + appFile: `${import.meta.dirname}/src/app.ts`, + }), + ], +}) +``` + +APIでは`AppType`というHonoの型をエクスポートします。この型を使ってOpenAPIドキュメントを生成します。 + +```typescript +// src/app.ts +import { Hono } from 'hono'; + +const app = new Hono() + .get('/hello', c => c.json({ message: 'Hello, World!' })); + +export type AppType = typeof app; +export default app; +``` + +引数に取る設定は次のとおりです。 + +```typescript +interface HtoConfig { + /** + * APIのタイトル + * Info Object の title に対応します。 + * https://spec.openapis.org/oas/v3.1.0#info-object + */ + title: string; + + /** + * OpenAPI のバージョンです。 + * @default "3.1" + */ + openapi: "3.1" | "3.0"; + + /** + * APIの説明 + * Info Object の description に対応します。 + * https://spec.openapis.org/oas/v3.1.0#info-object + */ + description: string; + + /** + * APIのバージョン + * Info Object の version に対応します。 + * https://spec.openapis.org/oas/v3.1.0#info-object + * @default "1.0.0" + */ + version: string; + + /** + * Hono app のファイルパス + * このファイルにある Hono app の型を使用して OpenAPI ドキュメントを生成します。 + */ + appFile: string; + + /** + * Hono app の型名 + * appFile にある Hono app の型名です。 + * @default "AppType" + */ + appType: string; + + /** + * 出力先のファイルパス + * @default "openapi.json" + */ + output?: string; + + /** + * tsconfig のファイルパス + * デフォルトでは カレントディレクトリから親ディレクトリを探索して見つかった tsconfig.json を使用します。 + */ + tsconfig?: string; + + /** + * watch モード + * @default false + */ + watchMode?: boolean; +} +``` + +### CLI + +CLIでは`hto`コマンドを使用します。 + +```bash +npx hto --title "My App" --app-file src/app.ts +``` + +設定はPluginと同じで、それぞれ次のように対応しています。 + +|CLI オプション|Plugin オプション| +|---|---| +|`-t`, `--title`|`title`| +|`-O`, `--openapi`|`openapi`| +|`-d`, `--description`|`description`| +|`-V`, `--app-version`|`version`| +|`-a`, `--app-file`|`appFile`| +|`-n`, `--app-type`|`appType`| +|`-o`, `--output`|`output`| +|`--tsconfig`|`tsconfig`| +|`-h`, `--help`|使用方法を表示します| +|`-v`, `--version`|バージョンを表示します| + +CLIを使用する場合は設定をファイルで指定することができます。 +サポートしているファイル形式は`js`, `mjs`, `cjs`, `ts`, `json`, `yaml`, `yml`です。 +また、`package.json`に`hto`フィールドを追加することで設定を指定することもできます。 + +```javascript +// hto.config.mjs +import { defineConfig } from 'hono-typia-openapi/config'; + +export default defineConfig({ + title: "My App", + appFile: `${import.meta.dirname}/src/app.ts`, +}); +``` + +### Hono app の作成方法 + +Hono app は`@honojs/typia-validator`を使用することで自動的に型が指定されます。 + +注意事項としてはメソッドチェーンの形式で書かないと型が正しく扱われないことです。これは Hono Client も同様なのですが、メソッドチェーンにしないと変数の型がスキーマを表す型にならないためです。 + +逆にこれを利用することでスキーマに出力しないエンドポイントを作ることもできます。 + +```typescript +import { Hono } from 'hono'; +import { typiaValidator } from '@honojs/typia-validator/http'; +import typia, { type tags } from 'typia'; + +interface User { + id: number & tags.Type<'uint32'>; + name: string & tags.MaxLength<255>; + age: number & tags.Type<'uint32'> & tags.Maximum<150>; +} + +const app = new Hono() + .get( + '/user', + typiaValidator('query', typia.http.createValidateQuery<{ age_from?: User["age"], age_to?: User["age"] }>()), + (c) => { + const { age_from, age_to } = c.req.valid('query'); + return c.json({ age_from, age_to }); + } + ).put( + '/user/:id', + typiaValidator('param', typia.createValidate<{ id: `${number}` }>()), + typiaValidator('body', typia.createValidate()), + (c) => { + const { id } = c.req.valid('param'); + const user = c.req.valid('body'); + if (id !== user.id) { + return c.status(400).json({ message: 'id does not match' }); + } + return c.json({ id, user }); + } + ) + +export type AppType = typeof app; +export default app; +``` + +### Swagger UI での表示 + +生成した OpenAPI ドキュメントは [@hono/swagger-ui](https://hono.dev/examples/swagger-ui) で表示することができます。ここでメソッドチェーンで書かないことによってスキーマに出力せずに swagger UI のエンドポイントを追加できます。 + +if文で環境変数を見ているのは開発環境でのみ swagger UI を表示するためです。さらに、識別子置換と Dead Code Elimination をバンドラーで行うことで本番環境に一切依存するコードがない完全なゼロランタイムが実現できます。 + +```typescript +import { Hono } from 'hono'; +import { typiaValidator } from '@honojs/typia-validator/http'; +import typia, { type tags } from 'typia'; + +interface User { + id: number & tags.Type<'uint32'>; + name: string & tags.MaxLength<255>; + age: number & tags.Type<'uint32'> & tags.Maximum<150>; +} + +const app = new Hono() + // エンドポイントを定義 + +if (process.env.NODE_ENV !== "production") { + const openapi = await import('node:fs/promises') + .then((fs) => fs.readFile('openapi.json', 'utf-8')) + .then(JSON.parse); + const { swaggerUI } = await import('@hono/swagger-ui'); + + app.get('/docs/openapi.json', (c) => c.json(openapi)); + app.get('/docs', swaggerUI(openapi)); +} + +export type AppType = typeof app; +export default app; +``` + +## 今後の予定 + +今後は次のような機能を追加する予定です。 + +- Typia の JSON シリアライザを簡単に扱えるようにするヘルパーの作成 +- Return Type を簡単に指定できるヘルパーの作成 +- エラー表示をわかりやすくする +- Description の自動生成 +- タグの指定 + +## まとめ + +Hono + Typia で OpenAPI ドキュメントを生成するライブラリを作成しました。これにより、型から完全なゼロランタイムで OpenAPI ドキュメントを生成することができます。 + + +[^1]: Typia 調べ + https://typia.io/docs/validators/is/#performance +[^2]: Vite, Rollup, Webpack, esbuild, Rspack, Rolldown, Farm diff --git a/src/content/tags/hono.json b/src/content/tags/hono.json new file mode 100644 index 0000000..346dffe --- /dev/null +++ b/src/content/tags/hono.json @@ -0,0 +1,15 @@ +{ + "name": "Hono", + "description": "Hono は、 TypeScript および JavaScript のためのオープンソースな Web フレームワークである。 Web 標準に従っているという特徴がある。", + "image": "./hono.svg", + "links": [ + { + "text": "Hono 公式サイト", + "url": "https://hono.dev/" + }, + { + "text": "Hono - GitHub", + "url": "https://github.com/honojs/hono" + } + ] +} diff --git a/src/content/tags/hono.svg b/src/content/tags/hono.svg new file mode 100644 index 0000000..f6c4689 --- /dev/null +++ b/src/content/tags/hono.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/content/tags/openapi.json b/src/content/tags/openapi.json new file mode 100644 index 0000000..c634170 --- /dev/null +++ b/src/content/tags/openapi.json @@ -0,0 +1,14 @@ +{ + "name": "Open API", + "description": "OpenAPIは、RESTful APIを設計・記述するための標準仕様です。APIの構造や動作を記述することで、開発者間のコミュニケーションを効率化し、API設計・実装・テストをスムーズに進めることができます。", + "links": [ + { + "text": "OpenAPI Specification v3.0.4", + "url": "https://spec.openapis.org/oas/v3.0.4" + }, + { + "text": "OpenAPI Specification v3.1.1", + "url": "https://spec.openapis.org/oas/v3.1.1" + } + ] +} diff --git a/src/content/tags/typia.json b/src/content/tags/typia.json new file mode 100644 index 0000000..92e03a2 --- /dev/null +++ b/src/content/tags/typia.json @@ -0,0 +1,15 @@ +{ + "name": "Typia", + "description": "Typia は TypeScript の型からランタイムの関数を生成するライブラリです。型から高速なバリデータやシリアライザを作成できます", + "image": "./typia.png", + "links": [ + { + "text": "Typia 公式サイト", + "url": "https://typia.io/" + }, + { + "text": "Typia - GitHub", + "url": "https://github.com/samchon/typia" + } + ] +} diff --git a/src/content/tags/typia.png b/src/content/tags/typia.png new file mode 100644 index 0000000..5835e97 Binary files /dev/null and b/src/content/tags/typia.png differ