HonoとRedocで始めるスキーマ駆動開発

## はじめに
前から思ってたんだけど、タイプセーフな言語で開発できて、ドキュメントの手動更新もいらなくて、しかもきれいに可視化できたらいいよなーって。

そしたら、どうやら`Bun`と`Hono`と`ReDoc`を使えばそれができちゃうらしいです。
![名称未設定ファイル.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/b1369ae1-7e0c-41f4-6143-450906cc9100.png)


## そもそもこいつらって何？

#### Bun
Bunは、最新のJavaScriptエコシステムに対応するため、ゼロから構築された新しいJavaScriptランタイムです、しかも非常に早くて、NodeJsより3倍近く速いらしいです。[公式ドキュメント](https://bun.sh/)

![A05B7BC0-AD7D-4AC0-8406-88AF4B8756C3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/a4251d36-b843-6fdc-def4-24ee51206661.jpeg)

この記事でBunを使うメリットは特にないんだけど、最近の流行りに乗っかりたいだけです。

#### Hono
高速で軽量、しかもWeb標準に基づいて作られてて、あらゆるJavaScriptランタイムをサポートしてるらしいです。
使ってみた感想としては、TypeScriptで書かれた現代のExpressって感じ。リリースされてから、NestJSを超える人気が出てるし、しかも作者は日本人。第2のRuby on Railsになることを期待してます。 [公式ドキュメント](https://hono.dev/)
![85DE1195-77E8-4137-AFD3-D2F45364CC56.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/5b0c5efa-f6d7-b3ce-fc7c-893e467a697b.jpeg)

#### ReDoc
ReDocはOpenAPI（Swagger）仕様を基にしてAPIのドキュメントを生成するツールです。HonoでSwaggerUIを使うのも簡単なんだけど、個人的にはReDocのUIの方がきれいなので、ReDocを使います。[リポジトリ](https://redocly.github.io/redoc/)
![978860CC-748E-4C2D-AD45-721DB002D876.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/38281c89-5e44-46bd-1764-47af1c6853a7.jpeg) 

## 事前準備

Bunをインストールします。

- Homebrew
```
% brew install oven-sh/bun/bun
```

- npm
```
% npm install -g bun
```

インストール完了後、バージョン確認できます。

```
% bun -v
> 1.1.33
```

## Honoのプロジェクト作成

`bunx`コマンド使って初期化します。

```bash
% bunx create-hono 
create-hono version 0.14.2
? Target directory bun-hono-api
? Which template do you want to use? cloudflare-workers
? Do you want to install project dependencies? yes
? Which package manager do you want to use? bun
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd bun-hono-api
```

初期化されたプロジェクトには、以下のようなエントリーファイルがあります。

```typescript:src/index.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

export default app
```

このコードは、ルート `/` にアクセスしたときに `Hello Hono!` を返すシンプルなAPIを定義しています。

次に、以下のコマンドを実行し、ブラウザで http://localhost:8787 にアクセスすると、Hello Hono! というメッセージが表示されます。

```
% bun run dev
wrangler dev

 ⛅️ wrangler 3.83.0
-------------------

⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
```


## @hono/zod-openapiインストール

```bash
% bun add @hono/zod-openapi
```

`@hono/zod-openapi`は、HonoとZodライブラリを統合し、OpenAPIに準拠したスキーマを定義・管理するためのツールです。
これを使うと、スキーマからAPIルートやバリデーションロジックを自動生成できます :point_up_tone1:
しかも、作者のお墨付きももらってます。

https://zenn.dev/yusukebe/articles/a00721f8b3b92e#zod-openapi

実際@hono/zod-openapi使ってルート `/`を改造しましょう。

```typescript:src/index.ts
import { OpenAPIHono as Hono, z, createRoute } from '@hono/zod-openapi';

const app = new Hono()

export const rootSchema = z.object({
  code: z.number().openapi({
    example: 200,
  }),
  message: z.string().openapi({
    example: 'Hello Hono!',
  }),
});

const rootRoute = createRoute({
  method: 'get',
  summary: 'root route',
  description: 'use health check',
  path: '/',
  request: {},
  responses: {
    200: {
      content: {
        'application/json': {
          schema: rootSchema,
        },
      },
      description: 'root router return Hello Hono',
    },
  },
  tags: ['Root'],
});

app.openapi(rootRoute, (c) => {
  return c.json({ code: 200, message: 'Hello Hono!' });
});

export default app
```
スキーマを追加することで、レスポンスの形式が厳密に定義されました。

再度以下のコマンドを実行し、ブラウザで http://localhost:8787 にアクセスすると、`{"code":200,"message":"Hello Hono!"} `というメッセージが表示されます。

```
% bun run dev
wrangler dev

 ⛅️ wrangler 3.83.0
-------------------

⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
```

レスポンスから`code`を削除してみます。

```diff_typescript:src/index.ts
app.openapi(rootRoute, (c) => {
-  return c.json({  message: 'Hello Hono!' });
});
```

![5B4F04D1-C7C9-4B24-B95F-47BADEFD8C5E_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/f7441fae-1f10-975a-78ae-b295ffd220f0.jpeg)
ちゃんとタイプエラーが出ました。

## ReDoc追加

まず、`src/index.ts`を`src/index.tsx`に変更します。

`wrangler.toml`のmainを下記のように修正。

```diff_toml:wrangler.toml
name = "bun-hono-api"
+ main = "src/index.tsx"
compatibility_date = "2024-10-30"
...略
```

```diff_typescript:src/index.tsx
... 略
app.openapi(rootRoute, (c) => {
  return c.json({ code: 200, message: 'Hello Hono!' });
});

+ app.doc('/openapi-spec.json', {
+   openapi: '3.0.0',
+   info: {
+     version: 'v1',
+     title: 'Sample ToDo list API',
+     description:
+     'This API exhibits how you can provide a comprehensible API documentation using [Hono](https://hono.dev), [Redoc](https://redocly.com/docs/redoc/).',
+     'x-logo': {
+      url: 'https://raw.githubusercontent.com/honojs/hono/main/docs/images/hono-title.png',
+      altText: 'Sample ToDo API logo',
+    },
+  },
+  tags: [
+     { name: 'Root', description: 'Root route' },
+   ],
+ });

+ app.get('/docs/redoc', (c) =>
+   c.html(
+     <html lang="en">
+       <head>
+         <meta charset="utf-8" />
+         <meta name="viewport" content="width=device-width, initial-scale=1" />
+         <meta name="description" content="SwaggerUI" />
+         <title>Sample API | Redoc</title>
+       </head>
+       <body>
+         <redoc spec-url="/openapi-spec.json"></redoc>
+         <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js">+ </script>
+       </body>
+    </html>,
+  ),
+ );

export default app
```

サーバー立ち上げて、http://localhost:8787/docs/redoc にアクセスすれば、ReDoc確認できると思います。
![B6DE5ABF-5676-4370-8F46-0D5D0D72E398.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/dd354c45-0822-ea42-04fc-27419e373c73.jpeg)

UIのカスタマイズする際には[公式ドキュメント](https://redocly.com/docs/redoc/deployment/html)を参考してください。

また、[swagger-ui](https://hono.dev/examples/swagger-ui)使用したい場合はこちらのドキュメントを参考にしてください。

## 補足

本記事では、Cloudflare Workersに対応したHonoプロジェクトを初期化しました。

以下のコマンドを実行すれば、Cloudflare Workersにデプロイできます。
```
bun run deploy
```
動作検証も済みです。

`AWS ECS`にデプロイしたい場合、下記の修正を加えれば、対応できます。

```diff_typescript:src/index.tsx
..略
- export default app;

+ export default {
+   port: 3000,
+   fetch: app.fetch,
+ };
```


```diff_javascript:package.json
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy --minify",
+   "start": "bun run src/index.tsx"
  }
```

```yaml:Dockerfile
# build
FROM oven/bun:latest

WORKDIR /app

COPY package.json ./

RUN bun install --production
COPY . .

EXPOSE 3000

CMD ["bun", "run", "start"]
```

## 最後に
ReDocの便利なカスタマイズ方法があれば、ぜひ教えてください。