0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

公式サンプルの API 実装全文

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

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'などの文字列リテラルが推論できなくなります。

型推論が効かない

ハンドラーを分離して記述する場合、例えば下記のように直接実装するとします。

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

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

型推論を効かせるための実装方法

型推論の恩恵を受けるために、@hono/zod-openapiが提供するRouteHandler<R>型を利用します。

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()の使用時に適切な型推論が働くようになります。

コード全文の掲載

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

ディレクトリ

$ tree -I node_modules
.
├── README.md
├── package-lock.json
├── package.json
├── src
│   ├── handler.ts
│   ├── index.ts
│   ├── route.ts
│   └── schema.ts
└── tsconfig.json

index.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

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

// こちらは分割しただけ。コード変更なし。
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

// こちらは分割しただけ。コード変更なし。
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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?