Zenn
🔥

Hono Takibi 0.2.0 🔥 z.infer などの対応

2025/01/05に公開

はじめに

Hono Advent Calendar 2024シリーズ2の5日目の記事です。

 前回の記事では、Hono Takibiを紹介しました。

https://x.com/mini_bg_pro_N/status/1871727536050409738

npm

https://www.npmjs.com/package/hono-takibi

GitHub

https://github.com/nakita628/hono-takibi

Hono Takibi 0.2.0

Hono Takibi0.2.0では、設定ファイルを使用して、生成されるコードの形式をカスタマイズできるようになりました。また、zod-to-openapiopenapiメソッドのサポートが一部追加され、OpenAPI仕様に準拠したスキーマ定義の出力が可能になりました。

 本記事では、これらの新機能と使用方法について詳しく解説します。

参考

Installation

npm install -D hono-takibi

Usage

  1. OpenAPI定義ファイル(YAMLまたはJSONファイル)を用意します。
.
└── openapi.yaml

openapi.yamlの内容を以下に示します(このサンプルは生成AIを使用して作成しています)。

openapi.yaml
openapi: 3.0.3
info:
  title: Sample API
  description: >
    これはサンプルのREST APIです。  
    OpenAPI 3.0以降に対応した仕様書の例としてご利用ください。
  version: "1.0.0"

servers:
  - url: https://api.example.com/v1
    description: 本番環境
  - url: https://stg-api.example.com/v1
    description: ステージング環境

paths:
  /users:
    get:
      summary: ユーザー一覧取得
      description: すべてのユーザーの一覧を取得します。
      tags:
        - Users
      responses:
        '200':
          description: ユーザー一覧の取得に成功
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"
        '400':
          description: 不正なリクエスト
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      summary: ユーザー登録
      description: 新規ユーザーを登録します。
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UserCreateRequest"
      responses:
        '201':
          description: 新規ユーザー作成に成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        '400':
          description: 不正なリクエスト
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /users/{userId}:
    parameters:
      - in: path
        name: userId
        schema:
          type: string
        required: true
        description: 取得または更新するユーザーのID
    get:
      summary: 特定ユーザー取得
      description: 指定したユーザーIDのユーザー情報を取得します。
      tags:
        - Users
      responses:
        '200':
          description: ユーザー情報の取得に成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        '404':
          description: ユーザーが見つからない
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    put:
      summary: ユーザー更新
      description: 指定したユーザーの情報を更新します。
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UserUpdateRequest"
      responses:
        '200':
          description: ユーザー情報の更新に成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        '400':
          description: 不正なリクエスト
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        '404':
          description: ユーザーが見つからない
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: ユーザー削除
      description: 指定したユーザーを削除します。
      tags:
        - Users
      responses:
        '204':
          description: ユーザー削除に成功 (レスポンスボディなし)
        '404':
          description: ユーザーが見つからない
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
          description: ユーザーID
          example: "12345"
        name:
          type: string
          description: ユーザーの名前
          example: "Taro Yamada"
        email:
          type: string
          description: メールアドレス
          example: "taro.yamada@example.com"
        createdAt:
          type: string
          format: date-time
          description: ユーザー作成日
          example: "2024-01-01T12:34:56Z"
      required:
        - id
        - name
        - email
        - createdAt

    UserCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: ユーザーの名前
          example: "Hanako Suzuki"
        email:
          type: string
          format: email
          description: メールアドレス
          example: "hanako.suzuki@example.com"
        password:
          type: string
          format: password
          description: パスワード
          example: "mypassword123"
      required:
        - name
        - email
        - password

    UserUpdateRequest:
      type: object
      properties:
        name:
          type: string
          description: 新しいユーザーの名前
          example: "Jiro Tanaka"
        email:
          type: string
          format: email
          description: 新しいメールアドレス
          example: "jiro.tanaka@example.com"
      required:
        - name
        - email

    Error:
      type: object
      properties:
        code:
          type: integer
          description: エラーコード
          example: 404
        message:
          type: string
          description: エラーメッセージ
          example: "Not Found"
      required:
        - code
        - message

hono-takibiを実行します。

npx hono-takibi openapi.yaml -o index.ts
.
├── index.ts
└── openapi.yaml

 以下のような、index.tsが生成されます。

import { createRoute, z } from '@hono/zod-openapi'

const userSchema = z
  .object({
    id: z.string().openapi({ example: '12345' }),
    name: z.string().openapi({ example: 'Taro Yamada' }),
    email: z.string().openapi({ example: 'taro.yamada@example.com' }),
    createdAt: z.string().datetime().openapi({ example: '2024-01-01T12:34:56Z' }),
  })
  .openapi('User')

const userCreateRequestSchema = z
  .object({
    name: z.string().openapi({ example: 'Hanako Suzuki' }),
    email: z.string().email().openapi({ example: 'hanako.suzuki@example.com' }),
    password: z.string().openapi({ example: 'mypassword123' }),
  })
  .openapi('UserCreateRequest')

const userUpdateRequestSchema = z
  .object({
    name: z.string().openapi({ example: 'Jiro Tanaka' }),
    email: z.string().email().openapi({ example: 'jiro.tanaka@example.com' }),
  })
  .openapi('UserUpdateRequest')

const errorSchema = z
  .object({
    code: z.number().int().openapi({ example: 404 }),
    message: z.string().openapi({ example: 'Not Found' }),
  })
  .openapi('Error')

export const getUsersRoute = createRoute({
  tags: ['Users'],
  method: 'get',
  path: '/users',
  summary: 'ユーザー一覧取得',
  description: 'すべてのユーザーの一覧を取得します。',
  responses: {
    200: {
      description: 'ユーザー一覧の取得に成功',
      content: { 'application/json': { schema: z.array(userSchema) } },
    },
    400: {
      description: '不正なリクエスト',
      content: { 'application/json': { schema: errorSchema } },
    },
  },
})

export const postUsersRoute = createRoute({
  tags: ['Users'],
  method: 'post',
  path: '/users',
  summary: 'ユーザー登録',
  description: '新規ユーザーを登録します。',
  request: {
    body: { required: true, content: { 'application/json': { schema: userCreateRequestSchema } } },
  },
  responses: {
    201: {
      description: '新規ユーザー作成に成功',
      content: { 'application/json': { schema: userSchema } },
    },
    400: {
      description: '不正なリクエスト',
      content: { 'application/json': { schema: errorSchema } },
    },
  },
})

export const getUsersUserIdRoute = createRoute({
  tags: ['Users'],
  method: 'get',
  path: '/users/{userId}',
  summary: '特定ユーザー取得',
  description: '指定したユーザーIDのユーザー情報を取得します。',
  responses: {
    200: {
      description: 'ユーザー情報の取得に成功',
      content: { 'application/json': { schema: userSchema } },
    },
    404: {
      description: 'ユーザーが見つからない',
      content: { 'application/json': { schema: errorSchema } },
    },
  },
})

export const putUsersUserIdRoute = createRoute({
  tags: ['Users'],
  method: 'put',
  path: '/users/{userId}',
  summary: 'ユーザー更新',
  description: '指定したユーザーの情報を更新します。',
  request: {
    body: { required: true, content: { 'application/json': { schema: userUpdateRequestSchema } } },
  },
  responses: {
    200: {
      description: 'ユーザー情報の更新に成功',
      content: { 'application/json': { schema: userSchema } },
    },
    400: {
      description: '不正なリクエスト',
      content: { 'application/json': { schema: errorSchema } },
    },
    404: {
      description: 'ユーザーが見つからない',
      content: { 'application/json': { schema: errorSchema } },
    },
  },
})

export const deleteUsersUserIdRoute = createRoute({
  tags: ['Users'],
  method: 'delete',
  path: '/users/{userId}',
  summary: 'ユーザー削除',
  description: '指定したユーザーを削除します。',
  responses: {
    204: { description: 'ユーザー削除に成功 (レスポンスボディなし)' },
    404: {
      description: 'ユーザーが見つからない',
      content: { 'application/json': { schema: errorSchema } },
    },
  },
})

Hono Takibi 0.2.0 の新機能

 Hono Takibi 0.2.0 では、以下の新機能を紹介します。

ディレクトリ構造

.
├── hono-takibi.json
├── index.ts
└── openapi.yaml

設定ファイル

hono-takibi.jsonは、hono-takibiの設定ファイルです。

{
  "schemaOptions": {
    "namingCase": "camelCase",
    "exportEnabled": false
  },
  "typeOptions": {
    "namingCase": "PascalCase",
    "exportEnabled": false
  }
}

オプション

Option Type Default Description
namingCase "camelCase" | "PascalCase" "camelCase" 生成されるスキーマ変数の命名規則
exportEnabled boolean false スキーマ定義をエクスポートするかどうか

Type Options

Option Type Default Description
namingCase "camelCase" | "PascalCase" "PascalCase" 生成される型定義の命名規則
exportEnabled boolean false 型定義をエクスポートするかどうか

オプションを指定して実行

 設定ファイルでオプションを指定することで、生成されるコードの形式をカスタマイズできます。以下は設定例です。

hono-takibi.json

{
  "schemaOptions": {
    "namingCase": "PascalCase",
    "exportEnabled": true
  },
  "typeOptions": {
    "namingCase": "PascalCase",
    "exportEnabled": true
  }
}

 以下のようなindex.tsが生成されます。

import { createRoute, z } from '@hono/zod-openapi'

const UserSchema = z
  .object({
    id: z.string().openapi({ example: '12345' }),
    name: z.string().openapi({ example: 'Taro Yamada' }),
    email: z.string().openapi({ example: 'taro.yamada@example.com' }),
    createdAt: z.string().datetime().openapi({ example: '2024-01-01T12:34:56Z' }),
  })
  .openapi('User')

const UserCreateRequestSchema = z
  .object({
    name: z.string().openapi({ example: 'Hanako Suzuki' }),
    email: z.string().email().openapi({ example: 'hanako.suzuki@example.com' }),
    password: z.string().openapi({ example: 'mypassword123' }),
  })
  .openapi('UserCreateRequest')

const UserUpdateRequestSchema = z
  .object({
    name: z.string().openapi({ example: 'Jiro Tanaka' }),
    email: z.string().email().openapi({ example: 'jiro.tanaka@example.com' }),
  })
  .openapi('UserUpdateRequest')

const ErrorSchema = z
  .object({
    code: z.number().int().openapi({ example: 404 }),
    message: z.string().openapi({ example: 'Not Found' }),
  })
  .openapi('Error')

export const schemas = {
  UserSchema,
  UserCreateRequestSchema,
  UserUpdateRequestSchema,
  ErrorSchema,
}

export type User = z.infer<typeof UserSchema>

export type UserCreateRequest = z.infer<typeof UserCreateRequestSchema>

export type UserUpdateRequest = z.infer<typeof UserUpdateRequestSchema>

export type Error = z.infer<typeof ErrorSchema>

export const getUsersRoute = createRoute({
  tags: ['Users'],
  method: 'get',
  path: '/users',
  summary: 'ユーザー一覧取得',
  description: 'すべてのユーザーの一覧を取得します。',
  responses: {
    200: {
      description: 'ユーザー一覧の取得に成功',
      content: { 'application/json': { schema: z.array(UserSchema) } },
    },
    400: {
      description: '不正なリクエスト',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

export const postUsersRoute = createRoute({
  tags: ['Users'],
  method: 'post',
  path: '/users',
  summary: 'ユーザー登録',
  description: '新規ユーザーを登録します。',
  request: {
    body: { required: true, content: { 'application/json': { schema: UserCreateRequestSchema } } },
  },
  responses: {
    201: {
      description: '新規ユーザー作成に成功',
      content: { 'application/json': { schema: UserSchema } },
    },
    400: {
      description: '不正なリクエスト',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

export const getUsersUserIdRoute = createRoute({
  tags: ['Users'],
  method: 'get',
  path: '/users/{userId}',
  summary: '特定ユーザー取得',
  description: '指定したユーザーIDのユーザー情報を取得します。',
  responses: {
    200: {
      description: 'ユーザー情報の取得に成功',
      content: { 'application/json': { schema: UserSchema } },
    },
    404: {
      description: 'ユーザーが見つからない',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

export const putUsersUserIdRoute = createRoute({
  tags: ['Users'],
  method: 'put',
  path: '/users/{userId}',
  summary: 'ユーザー更新',
  description: '指定したユーザーの情報を更新します。',
  request: {
    body: { required: true, content: { 'application/json': { schema: UserUpdateRequestSchema } } },
  },
  responses: {
    200: {
      description: 'ユーザー情報の更新に成功',
      content: { 'application/json': { schema: UserSchema } },
    },
    400: {
      description: '不正なリクエスト',
      content: { 'application/json': { schema: ErrorSchema } },
    },
    404: {
      description: 'ユーザーが見つからない',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

export const deleteUsersUserIdRoute = createRoute({
  tags: ['Users'],
  method: 'delete',
  path: '/users/{userId}',
  summary: 'ユーザー削除',
  description: '指定したユーザーを削除します。',
  responses: {
    204: { description: 'ユーザー削除に成功 (レスポンスボディなし)' },
    404: {
      description: 'ユーザーが見つからない',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

schemaOptions

schemaOptionsexportEnabledtrueにしたので、schemasをエクスポート。

⚠️ 重要な注意点

Hono Takibi0.2.0以降では、schemasのエクスポートは、schemaOptionsexportEnabledtrueにした場合のみ有効です。

export const schemas = {
  UserSchema,
  UserCreateRequestSchema,
  UserUpdateRequestSchema,
  ErrorSchema,
}

typeOptions

typeOptionsexportEnabledtrueにしたので、z.infer<typeof ***>などの、型定義をエクスポート。

export type User = z.infer<typeof UserSchema>

export type UserCreateRequest = z.infer<typeof UserCreateRequestSchema>

export type UserUpdateRequest = z.infer<typeof UserUpdateRequestSchema>

export type Error = z.infer<typeof ErrorSchema>

その他の改善点

zod-to-openapiopenapiメソッドのサポートが一部追加され、OpenAPI仕様に準拠したスキーマ定義の出力が可能になりました。

おわりに

Hono Takibi0.2.0では、設定ファイルを使用して、生成されるコードの形式をカスタマイズできるようになりました。また、zod-to-openapiopenapiメソッドのサポートが一部追加され、OpenAPI仕様に準拠したスキーマ定義の出力が可能になりました。

 これらの改善により、Hono Takibiの機能がさらに向上し、開発者のニーズに合わせたコード生成が可能になりました。今後はテストの拡充とドキュメントの整備を進めながら、継続的にメンテナンスを行い、より安定した機能を提供していく予定です。

参考リンク

Discussion

ログインするとコメントできます