Hono + Zod OpenAPIでルーティング周りをスッキリ書く

`@hono/zod-openapi`モジュールを使用し、APIサーバーとしてHonoを利用したときにルーティングとハンドラーを分割するのに少し苦慮したので記録を残しておきます。

# 公式サンプルの API 実装全文
まずは、公式サンプルとして、@hono/zod-openapi モジュールを用いた API 実装の全文をご覧ください。
ここではひとつのファイルに全ての処理をまとめています。

```ts
import { serve } from '@hono/node-server'
import { OpenAPIHono } from '@hono/zod-openapi'
import { createRoute } from '@hono/zod-openapi'
import { z } from '@hono/zod-openapi'

const ParamsSchema = z.object({
  id: z
    .string()
    .min(3)
    .openapi({
      param: {
        name: 'id',
        in: 'path',
      },
      example: '1212121',
    }),
})

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')

const route = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
      description: 'Retrieve the user',
    },
  },
})

const app = new OpenAPIHono()

app.openapi(route, (c) => {
  const { id } = c.req.valid('param')
  return c.json({
    id,
    age: 20,
    name: 'Ultra-man',
  })
})

app.doc('/doc', {
  openapi: '3.0.0',
  info: {
    version: '1.0.0',
    title: 'My API',
  },
})

serve({
  fetch: app.fetch,
  port: 3000
}, (info) => {
  console.log(`Server is running on http://localhost:${info.port}`)
})
```

# 1つのAPIルーティング宣言が長すぎる
上記のコードは、ひとつのAPIルーティングに関する定義がスキーマ定義、ルート設定、ハンドラー実装とすべてを1つのファイルにまとめて記述しており冗長です。
schema、route、handler に分割して記述したいところです。

しかし、handler 部分を単純に分離すると、`c.req.valid()` の型推論が正しく効かなくなります。
具体的には`'param'`や`'json'`などの文字列リテラルが推論できなくなります。

# 型推論が効かない
ハンドラーを分離して記述する場合、例えば下記のように直接実装するとします。

```ts
export const handler = (c) => {
  const { id } = c.req.valid('param')
  // ...
}
```

この場合、`c.req.valid()`の引数に何を渡しても静的解析でエラーが起こらなくなります。

# 型推論を効かせるための実装方法
型推論の恩恵を受けるために、`@hono/zod-openapi`が提供する`RouteHandler<R>`型を利用します。

```ts
import { type RouteHandler } from '@hono/zod-openapi'
import { type route } from './route.js'

export const handler: RouteHandler<typeof route> = (c) => {
  const { id } = c.req.valid('param')
  return c.json({
    id,
    age: 20,
    name: 'Ultra-man',
  })
}
```

このように`RouteHandler<typeof route>`と明示することで、routeの定義に基づいたリクエストの型情報がハンドラーに伝播されます。
これにより`c.req.valid()`の使用時に適切な型推論が働くようになります。

# コード全文の掲載
以下は、コードをファイル毎に分割した場合のディレクトリ構成と全文です。

### ディレクトリ
```sh
$ tree -I node_modules
.
├── README.md
├── package-lock.json
├── package.json
├── src
│   ├── handler.ts
│   ├── index.ts
│   ├── route.ts
│   └── schema.ts
└── tsconfig.json
```

### index.ts
```ts
import { serve } from '@hono/node-server'
import { OpenAPIHono } from '@hono/zod-openapi'
import { handler } from './handler.js'
import { route } from './route.js'

const app = new OpenAPIHono()

// 呼び出し元はルーティングとハンドラーを宣言するだけにとどまる
app.openapi(route, handler)

app.doc('/doc', {
  openapi: '3.0.0',
  info: {
    version: '1.0.0',
    title: 'My API',
  },
})

serve({
  fetch: app.fetch,
  port: 3000
}, (info) => {
  console.log(`Server is running on http://localhost:${info.port}`)
})
```

### handler.ts
```ts
import { type route } from './route.js'
import { type RouteHandler } from '@hono/zod-openapi'

// RouteHandlerでrouteをジェネリクス宣言することで型推論する
export const handler: RouteHandler<typeof route> = (c) => {
  const { id } = c.req.valid('param')
  return c.json({
    id,
    age: 20,
    name: 'Ultra-man',
  })
}
```

### route.ts
```ts
// こちらは分割しただけ。コード変更なし。
import { createRoute } from '@hono/zod-openapi'
import { ParamsSchema, UserSchema } from './schema.js'

export const route = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
      description: 'Retrieve the user',
    },
  },
})
```

### schema.ts
```ts
// こちらは分割しただけ。コード変更なし。
import { z } from '@hono/zod-openapi'

export const ParamsSchema = z.object({
  id: z
    .string()
    .min(3)
    .openapi({
      param: {
        name: 'id',
        in: 'path',
      },
      example: '1212121',
    }),
})

export 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')
```

# 参考
[w3cj/hono-open-api-starter: A starter template for building fully documented type-safe JSON APIs with Hono and Open API](https://github.com/w3cj/hono-open-api-starter)