Hono に入門して OpenAPI でバックエンド開発をしてみた

今回は Rails 開発を主軸にしている私が、 Typescript に親しむことを目的に、最近話題のフレームワークの Hono に入門して、OpenAPI を使用したサーバーサイド開発をしていこうと思います。

:::note info
この記事では、アーキテクチャ設計とかそういう保守・運用を見据えたことは言及をしません。あくまで、Rubyist が Typescript に親しむためのアウトプットとしてご覧ください。
:::

## 導入
今回は、npm や yarn ではなく、Bun という Zig 言語で書かれている、超高速な Node.js 互換の実行環境＋ビルドツール＋パッケージマネージャで環境構築をしてみました。

理由としては、Node.js に替わる新しい JS ランタイムで、Typescriptをネイティブでサポートしており、「とにかく爆速らしい」という噂を聞きつけ、実際に使ってみようと思ったためです。それ以上に深い理由はないです。

https://bun.sh

公式は curl でのインストールを提示していますが、Homebrew でもいけるという情報があり、以下のようにインストールをしました。

```zsh
brew tap oven-sh/bun
brew install bun
```
そのあとは、Honoの公式でも書いてあるように、
```zsh
bun create hono@latest
```
というコマンドを打ちます。
他のパッケージマネージャーのやり方は以下の公式ドキュメントを参考に。

https://hono-ja.pages.dev/docs/

```zsh
create-hono version 0.19.1
✔ Target directory hono-sample
✔ Which template do you want to use? bun
✔ 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 hono-sample
```

こんな感じでプロジェクトが作成したら、サーバーがちゃんと動くか確かめてみましょう。

```zsh
❯ bun dev
$ bun run --hot src/index.ts
Started development server: http://localhost:3000
```

これで、 `Hello hono!` が表示されていればOKです！

## API開発をしてみた

では、実際にHonoでAPIを開発してみましょう。
今回採用したファイル構成はMVCに近い感じです。

```
.
├── app.ts // アプリ全体の設定
├── bun.lock
├── package.json
├── README.md
├── src
│   ├── controller
│   │   └── diary.ts // 実際のCRUD操作
│   ├── model
│   │   └── diary.ts // zod で型やバリデーションルール作成
│   └── routes
│       └── diary.ts // エンドポイントのルーティング
└── tsconfig.json
```

今回は本当に簡単な例として、日記アプリのバックエンド側を作成してみようということでやっていきましょう。

まずは、スキーマ設定です。zod だと簡単にできます。

```ts
// src/model/diary.ts
import { z } from '@hono/zod-openapi'

export const DiaryEntrySchema = z
  .object({
    id: z
      .number()
      .int()
      .positive()
      .openapi({ example: 1 }),
    title: z
      .string()
      .min(1)
      .openapi({ example: '今日の出来事' }),
    content: z
      .string()
      .min(1)
      .openapi({ example: 'とても楽しい一日だった。' }),
    date: z
      .string()
      .openapi({ example: '2025-06-12' }),
  })
  .openapi('DiaryEntry')

export const NewDiaryEntrySchema = DiaryEntrySchema.omit({ id: true }).openapi(
  'NewDiaryEntry'
)

export type DiaryEntry = z.infer<typeof DiaryEntrySchema>
export type NewDiaryEntry = z.infer<typeof NewDiaryEntrySchema> 
```

Zod スキーマに`openapi()`を付与するだけで、 OpenAPI の情報を付与することができるので非常に便利でした。型やバリデーションが簡単に設定できる & コードも見やすいので助かりますね。

続きまして実際にCRUD操作をしていこうと思います。今回は Create を例に取り上げます。

デモ用に以下のデータがあると仮定します。
```ts
let entries: DiaryEntry[] = [
  {
    id: 1,
    title: '初めての日記',
    content: 'こんにちは、世界！',
    date: new Date().toISOString().slice(0, 10),
  },
]
```

実際のロジックは、以下のようになります。

```ts
// src/controller/diary.ts
export const createEntry = (data: NewDiaryEntry): DiaryEntry => {
  const entry: DiaryEntry = { id: nextId++, ...data }
  entries.push(entry)
  return entry
}
```

次にエンドポイントの設定をします。
```ts
const createRouteDef = createRoute({
  method: 'post',
  path: '/diary',
  request: {
    body: {
      content: {
        'application/json': {
          schema: NewDiaryEntrySchema, // バリデーション
        },
      },
    },
  },
  responses: {
    201: {
      description: '作成成功',
      content: {
        'application/json': {
          schema: DiaryEntrySchema, // バリデーション
        },
      },
    },
  },
})

app.openapi(createRouteDef, async (c) => {
  const data = c.req.valid('json') //　JSONをバリデーション付きで取得
  const entry = createEntry(data) // 受け取ったデータを使って、新しい日記を作成
  return c.json(entry, 201) // レスポンスをする、成功すれば201が返る
})
```

では実際に、バリデーションが機能しているか、APIを叩いてみましょう。あえて、date を string ではなく number で記述して POST します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026821/07883884-c074-4a8d-b1c9-6344153d16ce.png)

すると無事、ZodError で リクエストが失敗し、バリデーションが機能していることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026821/c51ff1b6-0306-4de5-85d1-6382a7f167d2.png)

普通に Hono で記述する場合より、少し量が多くなってしまうものの、ドキュメント生成＋自動バリデーションをしながら記述できるという面で、`@hono/zod-openapi` は非常に優秀だなと感じました。

## 感想
Typescript バックエンド入門で Hono を触りましたが、実際にサーバーサイド開発を試してみて、「ルーティング→バリデーション→レスポンス」までを一筆書きのように組み立てられる快適さに驚きました。
コード量を比較的抑えつつ型安全もしっかり担保されるので、Rails 脳でも迷わず実装できるとわかったのが、一番の収穫です。

また、Bun を採用してみてわかったこととしては、`bun create hono@latest` が本当に爆速でした。僕は普段 nodebrew + npm を使用していますが、パッケージのインストール時間がちょっと長くて煩わしい時がありますが、Bun はすぐインストールが終わりました。今後、開発が進むと思うので、Bun を使用した環境構築も選択肢の一つになりそうです。