🔥

Hono Takibi 0.50

2025/02/24に公開

Hono Takibi 0.5.0

npm

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

GitHub

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

Hono Takibi 0.5.0の変更点

 0.4.*では、以下のように設定できましたが、誤ってappオプションを指定して、生成されるコードに影響を及ぼすことを考え、0.5.0以降ではappオプションを削除しました。

{
  "schema": {
    "name": "PascalCase",
    "export": false
  },
  "type": {
    "name": "PascalCase",
    "export": false
  },
  "app": {
    "output": true,
    "test": true,
    "basePath": "api",
    "env": "process.env.NODE_ENV"
  }
}

 0.5.0以降では、appオプションを削除したため、以下のようにオプションを指定して、コマンドで実行をお願いします。

Options

Option Description
-template アプリケーションの雛形の生成を有効にする。
-test テストファイルを生成する。
--base-path APIのエンドポイントを指定する。
--env 開発モードを決定するために使用される環境変数を指定する。(デフォルト process.env.NODE_ENV)

⚠️ -templateオプションを使用する場合、有効なディレクトリパスを指定する必要があります。コマンドを実行する前に、そのディレクトリが存在することを確認してください。

Example

npx hono-takibi openapi.yaml -o project/routes.ts -template -test --basePath 'api' --env 'process.env.NODE_ENV'

使用例

openapi.yamlを用意します。

.
└── openapi.yaml
openapi.yaml
openapi: 3.1.0
info:
  title: Hono API
  version: v1

components:
  schemas:
    Error:
      type: object
      properties:
        message:
          type: string
      required:
        - message
    Post:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier of the post
        post:
          type: string
          description: Content of the post
          minLength: 1
          maxLength: 140
        createdAt:
          type: string
          format: date-time
          description: Timestamp when the post was created
        updatedAt:
          type: string
          format: date-time
          description: Timestamp when the post was last updated
      required:
        - id
        - post
        - createdAt
        - updatedAt

tags:
  - name: Hono
    description: Endpoints related to general Hono operations
  - name: Post
    description: Endpoints for creating, retrieving, updating, and deleting posts

paths:
  /:
    get:
      tags:
        - Hono
      summary: Welcome message
      description: Retrieve a simple welcome message from the Hono API.
      responses:
        '200':
          description: Successful response with a welcome message.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Hono🔥
                required:
                  - message

  /posts:
    post:
      tags:
        - Post
      summary: Create a new post
      description: Submit a new post with a maximum length of 140 characters.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                post:
                  type: string
                  description: Content of the post
                  minLength: 1
                  maxLength: 140
              required:
                - post
            example:
              post: "This is my first post!"
      responses:
        '201':
          description: Post successfully created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Created
        '400':
          description: Invalid request due to bad input.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Post content is required and must be between 1 and 140 characters.
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: An unexpected error occurred. Please try again later.

    get:
      tags:
        - Post
      summary: Retrieve a list of posts
      description: Retrieve a paginated list of posts. Specify the page number and the number of posts per page.
      parameters:
        - in: query
          name: page
          required: true
          schema:
            type: integer
            minimum: 0
            default: 1
            example: 1
          description: The page number to retrieve. Must be a positive integer. Defaults to 1.
        - in: query
          name: rows
          required: true
          schema:
            type: integer
            minimum: 0
            default: 10
            example: 10
          description: The number of posts per page. Must be a positive integer. Defaults to 10.
      responses:
        '200':
          description: Successfully retrieved a list of posts.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Post'
              example:
                - id: "123e4567-e89b-12d3-a456-426614174000"
                  post: "Hello world!"
                  createdAt: "2024-12-01T12:34:56Z"
                  updatedAt: "2024-12-02T14:20:00Z"
        '400':
          description: Invalid request due to bad input.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid page or rows parameter. Both must be positive integers.
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: An unexpected error occurred. Please try again later.

  /posts/{id}:
    put:
      tags:
        - Post
      summary: Update an existing post
      description: Update the content of an existing post identified by its unique ID.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the post.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                post:
                  type: string
                  description: Updated content for the post
                  minLength: 1
                  maxLength: 140
              required:
                - post
            example:
              post: "Updated post content."
      responses:
        '204':
          description: Post successfully updated.
        '400':
          description: Invalid input.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Post content is required and must be between 1 and 140 characters.
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: An unexpected error occurred. Please try again later.

    delete:
      tags:
        - Post
      summary: Delete a post
      description: Delete an existing post identified by its unique ID.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
            example: 123e4567-e89b-12d3-a456-426614174000
          description: Unique identifier of the post.
      responses:
        '204':
          description: Post successfully deleted.
        '400':
          description: Invalid input.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid post ID.
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: An unexpected error occurred. Please try again later.

 オプションを指定して、コマンドを実行します。

pnpm hono-takibi openapi.yaml -o src/routes.ts -template --base-path = "api"

 コマンドを実行すると、以下のようにファイルが生成されます。

.
├── src
│   ├── handler
│   │   ├── index_handler.ts
│   │   └── posts_handler.ts
│   ├── index.ts
│   └── routes.ts
├── openapi.yaml
├── package.json
└── pnpm-lock.yaml

 生成されたファイルを確認します。

 ルート定義が生成されます。詳細は省略します。

src/routes.ts
import { createRoute, z } from '@hono/zod-openapi'

const ErrorSchema = z.object({ message: z.string() }).openapi('Error')

const PostSchema = z
  .object({
    id: z.string().uuid(),
    post: z.string().min(1).max(140),
    createdAt: z.string().datetime(),
    updatedAt: z.string().datetime(),
  })
  .openapi('Post')

export const getRoute = createRoute({
  tags: ['Hono'],
  method: 'get',
  path: '/',
  summary: 'Welcome message',
  description: 'Retrieve a simple welcome message from the Hono API.',
  responses: {
    200: {
      description: 'Successful response with a welcome message.',
      content: {
        'application/json': {
          schema: z.object({ message: z.string().openapi({ example: 'Hono🔥' }) }),
        },
      },
    },
  },
})

export const postPostsRoute = createRoute({
  tags: ['Post'],
  method: 'post',
  path: '/posts',
  summary: 'Create a new post',
  description: 'Submit a new post with a maximum length of 140 characters.',
  request: {
    body: {
      required: true,
      content: { 'application/json': { schema: z.object({ post: z.string().min(1).max(140) }) } },
    },
  },
  responses: {
    201: {
      description: 'Post successfully created.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

export const getPostsRoute = createRoute({
  tags: ['Post'],
  method: 'get',
  path: '/posts',
  summary: 'Retrieve a list of posts',
  description:
    'Retrieve a paginated list of posts. Specify the page number and the number of posts per page.',
  request: {
    query: z.object({
      page: z.string().pipe(z.coerce.number().int().min(0).default(1).openapi({ example: 1 })),
      rows: z.string().pipe(z.coerce.number().int().min(0).default(10).openapi({ example: 10 })),
    }),
  },
  responses: {
    200: {
      description: 'Successfully retrieved a list of posts.',
      content: { 'application/json': { schema: z.array(PostSchema) } },
    },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

export const putPostsIdRoute = createRoute({
  tags: ['Post'],
  method: 'put',
  path: '/posts/{id}',
  summary: 'Update an existing post',
  description: 'Update the content of an existing post identified by its unique ID.',
  request: {
    body: {
      required: true,
      content: { 'application/json': { schema: z.object({ post: z.string().min(1).max(140) }) } },
    },
    params: z.object({ id: z.string().uuid() }),
  },
  responses: {
    204: { description: 'Post successfully updated.' },
    400: {
      description: 'Invalid input.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

export const deletePostsIdRoute = createRoute({
  tags: ['Post'],
  method: 'delete',
  path: '/posts/{id}',
  summary: 'Delete a post',
  description: 'Delete an existing post identified by its unique ID.',
  request: {
    params: z.object({
      id: z
        .string()
        .uuid()
        .openapi({
          param: { name: 'id', in: 'path' },
          example: '123e4567-e89b-12d3-a456-426614174000',
        }),
    }),
  },
  responses: {
    204: { description: 'Post successfully deleted.' },
    400: {
      description: 'Invalid input.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: ErrorSchema } },
    },
  },
})

src/index.tsを確認します。自動生成されたものです。ミドルウェアなどは、必要に応じて設定する必要があります。

import { OpenAPIHono } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
import {
  getRoute,
  postPostsRoute,
  getPostsRoute,
  putPostsIdRoute,
  deletePostsIdRoute,
} from './route.ts'
import { getRouteHandler } from './handler/index_handler.ts'
import {
  postPostsRouteHandler,
  getPostsRouteHandler,
  putPostsIdRouteHandler,
  deletePostsIdRouteHandler,
} from './handler/posts_handler.ts'

const app = new OpenAPIHono().basePath('api')

export const api = app
  .openapi(getRoute, getRouteHandler)
  .openapi(postPostsRoute, postPostsRouteHandler)
  .openapi(getPostsRoute, getPostsRouteHandler)
  .openapi(putPostsIdRoute, putPostsIdRouteHandler)
  .openapi(deletePostsIdRoute, deletePostsIdRouteHandler)

const isDev = process.env.NODE_ENV === 'development'

if (isDev) {
  app
    .doc('/doc', {
      openapi: '3.1.0',
      info: { title: 'Hono API', version: 'v1' },
      tags: [
        { name: 'Hono', description: 'Endpoints related to general Hono operations' },
        {
          name: 'Post',
          description: 'Endpoints for creating, retrieving, updating, and deleting posts',
        },
      ],
    })
    .get('/ui', swaggerUI({ url: '/api/doc' }))
}

export type AddType = typeof api

export default app

src/handlerディレクトリに、各ルートのハンドラーが生成されます。ビジネスロジックは、開発者が実装する必要があります。

src/handler/index_handler.ts

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

export const getRouteHandler: RouteHandler<typeof getRoute> = async (c) => {}

src/handler/posts_handler.ts

import type { RouteHandler } from '@hono/zod-openapi'
import type {
  postPostsRoute,
  getPostsRoute,
  putPostsIdRoute,
  deletePostsIdRoute,
} from '../route.ts'

export const postPostsRouteHandler: RouteHandler<typeof postPostsRoute> = async (c) => {}

export const getPostsRouteHandler: RouteHandler<typeof getPostsRoute> = async (c) => {}

export const putPostsIdRouteHandler: RouteHandler<typeof putPostsIdRoute> = async (c) => {}

export const deletePostsIdRouteHandler: RouteHandler<typeof deletePostsIdRoute> = async (c) => {}

 オプションを指定して、雛形が生成されました。次にロジックを書いていきます。

 ミドルウェアやポート番号を設定していきます。

src/index.ts

import { OpenAPIHono } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
import {
  getRoute,
  postPostsRoute,
  getPostsRoute,
  putPostsIdRoute,
  deletePostsIdRoute,
} from './route.ts'
import { getRouteHandler } from './handler/index_handler.ts'
import {
  postPostsRouteHandler,
  getPostsRouteHandler,
  putPostsIdRouteHandler,
  deletePostsIdRouteHandler,
} from './handler/posts_handler.ts'
import { logger } from 'hono/logger'
import { serve } from '@hono/node-server'

const app = new OpenAPIHono().basePath('api')

app.use('*', logger())
app.use('*', (c, next) => {
  console.log(`${c.req.method} ${c.req.url}`)
  return next()
})

app.use('*', async (c, next) => {
  try {
    await next()
  } catch (e) {
    return c.json({ error: (e as Error).message }, 500)
  }
})

export const api = app
  .openapi(getRoute, getRouteHandler)
  .openapi(postPostsRoute, postPostsRouteHandler)
  .openapi(getPostsRoute, getPostsRouteHandler)
  .openapi(putPostsIdRoute, putPostsIdRouteHandler)
  .openapi(deletePostsIdRoute, deletePostsIdRouteHandler)

const isDev = process.env.NODE_ENV === 'development'

if (isDev) {
  app
    .doc('/doc', {
      openapi: '3.1.0',
      info: { title: 'Hono API', version: 'v1' },
      tags: [
        { name: 'Hono', description: 'Endpoints related to general Hono operations' },
        {
          name: 'Post',
          description: 'Endpoints for creating, retrieving, updating, and deleting posts',
        },
      ],
    })
    .get('/ui', swaggerUI({ url: '/api/doc' }))
}

export type AddType = typeof api

const port = 3000
console.log(`Server is running on http://localhost:${port}`)

serve({
  fetch: app.fetch,
  port,
})

 続いて、handlerを実装していきます。

src/handler/index_handler.ts

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

export const getRouteHandler: RouteHandler<typeof getRoute> = async (c) => {
  return c.json({ message: 'Hono🔥 Drizzle' })
}

src/handler/posts_handler.ts

import type { RouteHandler } from '@hono/zod-openapi'
import type {
  postPostsRoute,
  getPostsRoute,
  putPostsIdRoute,
  deletePostsIdRoute,
} from '../route.ts'
import db from '../../db'
import { Post } from '../../db/schema'
import { eq, desc } from 'drizzle-orm'

export const postPostsRouteHandler: RouteHandler<typeof postPostsRoute> = async (c) => {
  const { post } = c.req.valid('json')
  await db.insert(Post).values({ post })
  return c.json({ message: 'Created' }, 201)
}

export const getPostsRouteHandler: RouteHandler<typeof getPostsRoute> = async (c) => {
  const { page, rows } = c.req.valid('query')
  const limit = rows
  const offset = (page - 1) * rows
  const posts = await db
    .select()
    .from(Post)
    .orderBy(desc(Post.createdAt))
    .limit(limit)
    .offset(offset)
  return c.json(posts, 200)
}

export const putPostsIdRouteHandler: RouteHandler<typeof putPostsIdRoute> = async (c) => {
  const { id } = c.req.valid('param')
  const { post } = c.req.valid('json')
  await db.update(Post).set({ post }).where(eq(Post.id, id))
  return new Response(null, { status: 204 })
}

export const deletePostsIdRouteHandler: RouteHandler<typeof deletePostsIdRoute> = async (c) => {
  const { id } = c.req.valid('param')
  await db.delete(Post).where(eq(Post.id, id))
  return new Response(null, { status: 204 })
}

 Swagger UIを確認します。

OpenAPI定義から、雛形を作成させ、ビジネスロジックの実装に集中できるようになったと思います。

OpenAIから、OpenAPI定義を生成させ、雛形を生成する

プロンプト

あなたは「REST APIのOpenAPI仕様」を生成する専門家です。以下の要件をすべて満たすOpenAPIドキュメントを YAML形式 で作成してください。なお、OpenAPIのバージョンは3.0以上 とし、誤りや不足がないよう注意深く出力してください。追加条件で、複雑なスキーマを定義してください。

 OpenAIから、OpenAPI定義を生成させ、雛形を生成します。

openapi.yaml
openapi: "3.0.3"
info:
  title: "Sample Complex REST API"
  version: "1.0.0"
  description: >
    このAPIは、複雑なスキーマ定義(入れ子になったオブジェクト、配列、条件付き型など)を含むサンプルのREST APIです。
servers:
  - url: "https://api.example.com/v1"
paths:
  /items:
    get:
      summary: "アイテムの一覧を取得"
      operationId: "getItems"
      responses:
        '200':
          description: "アイテムの配列を返却"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Item"
    post:
      summary: "新しいアイテムを作成"
      operationId: "createItem"
      requestBody:
        description: "作成するアイテムの情報"
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Item"
      responses:
        '201':
          description: "アイテム作成成功"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Item"
  /items/{itemId}:
    parameters:
      - $ref: "#/components/parameters/itemId"
    get:
      summary: "指定したIDのアイテムを取得"
      operationId: "getItemById"
      responses:
        '200':
          description: "アイテムの詳細情報を返却"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Item"
        '404':
          description: "アイテムが存在しない"
    put:
      summary: "指定したIDのアイテムを更新"
      operationId: "updateItem"
      requestBody:
        description: "更新するアイテムの情報"
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ItemUpdate"
      responses:
        '200':
          description: "アイテム更新成功"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Item"
        '404':
          description: "アイテムが存在しない"
    delete:
      summary: "指定したIDのアイテムを削除"
      operationId: "deleteItem"
      responses:
        '204':
          description: "アイテム削除成功(内容なし)"
        '404':
          description: "アイテムが存在しない"
components:
  schemas:
    Item:
      type: object
      required:
        - id
        - name
        - details
      properties:
        id:
          type: string
          description: "ユニークなアイテムID(UUID形式など)"
          example: "123e4567-e89b-12d3-a456-426614174000"
        name:
          type: string
          description: "アイテム名"
          example: "サンプルアイテム"
        description:
          type: string
          description: "アイテムの詳細な説明"
          example: "このアイテムはサンプルとして使用されます。"
        details:
          type: object
          description: "アイテムに関する詳細情報"
          required:
            - createdAt
            - updatedAt
          properties:
            createdAt:
              type: string
              format: date-time
              description: "作成日時"
              example: "2021-07-21T17:32:28Z"
            updatedAt:
              type: string
              format: date-time
              description: "更新日時"
              example: "2021-07-22T11:15:45Z"
            attributes:
              type: array
              description: "各種属性情報の配列"
              minItems: 1
              maxItems: 10
              items:
                $ref: "#/components/schemas/Attribute"
        tags:
          type: array
          description: "アイテムに紐付くタグの一覧"
          minItems: 1
          maxItems: 5
          items:
            type: string
          example: ["new", "sale"]
        metadata:
          type: object
          description: "追加のメタデータ情報"
          properties:
            attributeGroups:
              type: array
              description: "グループごとに分類された属性情報"
              minItems: 1
              maxItems: 3
              items:
                type: array
                description: "属性のグループ"
                minItems: 1
                maxItems: 5
                items:
                  $ref: "#/components/schemas/Attribute"
            history:
              type: array
              description: "アイテムの更新履歴"
              items:
                type: object
                required:
                  - updatedAt
                  - changes
                properties:
                  updatedAt:
                    type: string
                    format: date-time
                    description: "更新日時"
                  changes:
                    type: array
                    description: "変更されたフィールドの一覧"
                    items:
                      type: string
    ItemUpdate:
      type: object
      description: "アイテム更新時に使用するスキーマ。部分更新可能なフィールドを定義。"
      properties:
        name:
          type: string
          description: "更新後のアイテム名"
          example: "更新されたアイテム名"
        description:
          type: string
          description: "更新後の詳細説明"
          example: "更新された説明文。"
        details:
          type: object
          description: "更新対象の詳細情報"
          properties:
            updatedAt:
              type: string
              format: date-time
              description: "更新日時"
              example: "2021-07-22T11:15:45Z"
            attributes:
              type: array
              description: "更新対象の属性情報配列"
              minItems: 1
              maxItems: 10
              items:
                $ref: "#/components/schemas/Attribute"
          additionalProperties: false
        tags:
          type: array
          description: "更新後のタグ配列"
          minItems: 1
          maxItems: 5
          items:
            type: string
        metadata:
          type: object
          description: "更新対象の追加メタデータ情報"
          properties:
            attributeGroups:
              type: array
              description: "グループごとに分類された属性情報"
              minItems: 1
              maxItems: 3
              items:
                type: array
                description: "属性のグループ"
                minItems: 1
                maxItems: 5
                items:
                  $ref: "#/components/schemas/Attribute"
            history:
              type: array
              description: "更新履歴の追加情報"
              items:
                type: object
                required:
                  - updatedAt
                  - changes
                properties:
                  updatedAt:
                    type: string
                    format: date-time
                    description: "更新日時"
                  changes:
                    type: array
                    description: "変更されたフィールドの一覧"
                    items:
                      type: string
          additionalProperties: false
    Attribute:
      type: object
      description: "アイテムの属性情報。値は文字列、数値、または真偽値を取る可能性がある。"
      required:
        - key
        - value
      properties:
        key:
          type: string
          description: "属性のキー"
          example: "color"
        value:
          description: "属性の値。型は文字列、数値、または真偽値のいずれか。"
          oneOf:
            - type: string
              example: "red"
            - type: number
              example: 10
            - type: boolean
              example: true
      additionalProperties: false
  parameters:
    itemId:
      name: itemId
      in: path
      description: "対象のアイテムID"
      required: true
      schema:
        type: string
route.ts
import { createRoute, z } from '@hono/zod-openapi'

const AttributeSchema = z
  .object({
    key: z.string().openapi({ example: 'color' }),
    value: z.union([
      z.string().openapi({ example: 'red' }),
      z.number().openapi({ example: 10 }),
      z.boolean(),
    ]),
  })
  .openapi('Attribute')

const ItemSchema = z
  .object({
    id: z.string().openapi({ example: '123e4567-e89b-12d3-a456-426614174000' }),
    name: z.string().openapi({ example: 'サンプルアイテム' }),
    description: z
      .string()
      .openapi({ example: 'このアイテムはサンプルとして使用されます。' })
      .optional(),
    details: z.object({
      createdAt: z.string().datetime().openapi({ example: '2021-07-21T17:32:28Z' }),
      updatedAt: z.string().datetime().openapi({ example: '2021-07-22T11:15:45Z' }),
      attributes: z.array(AttributeSchema).optional(),
    }),
    tags: z.array(z.string()).min(1).max(5).optional(),
    metadata: z
      .object({
        attributeGroups: z.array(z.array(AttributeSchema).min(1).max(5)).min(1).max(3),
        history: z.array(
          z.object({ updatedAt: z.string().datetime(), changes: z.array(z.string()) }),
        ),
      })
      .partial()
      .optional(),
  })
  .openapi('Item')

const ItemUpdateSchema = z
  .object({
    name: z.string().openapi({ example: '更新されたアイテム名' }),
    description: z.string().openapi({ example: '更新された説明文。' }),
    details: z
      .object({
        updatedAt: z.string().datetime().openapi({ example: '2021-07-22T11:15:45Z' }),
        attributes: z.array(AttributeSchema),
      })
      .partial(),
    tags: z.array(z.string()).min(1).max(5),
    metadata: z
      .object({
        attributeGroups: z.array(z.array(AttributeSchema).min(1).max(5)).min(1).max(3),
        history: z.array(
          z.object({ updatedAt: z.string().datetime(), changes: z.array(z.string()) }),
        ),
      })
      .partial(),
  })
  .partial()
  .openapi('ItemUpdate')

export const getItemsRoute = createRoute({
  tags: [],
  method: 'get',
  path: '/items',
  summary: 'アイテムの一覧を取得',
  responses: {
    200: {
      description: 'アイテムの配列を返却',
      content: { 'application/json': { schema: z.array(ItemSchema) } },
    },
  },
})

export const postItemsRoute = createRoute({
  tags: [],
  method: 'post',
  path: '/items',
  summary: '新しいアイテムを作成',
  request: { body: { required: true, content: { 'application/json': { schema: ItemSchema } } } },
  responses: {
    201: {
      description: 'アイテム作成成功',
      content: { 'application/json': { schema: ItemSchema } },
    },
  },
})

export const getItemsItemIdRoute = createRoute({
  tags: [],
  method: 'get',
  path: '/items/{itemId}',
  summary: '指定したIDのアイテムを取得',
  responses: {
    200: {
      description: 'アイテムの詳細情報を返却',
      content: { 'application/json': { schema: ItemSchema } },
    },
    404: { description: 'アイテムが存在しない' },
  },
})

export const putItemsItemIdRoute = createRoute({
  tags: [],
  method: 'put',
  path: '/items/{itemId}',
  summary: '指定したIDのアイテムを更新',
  request: {
    body: { required: true, content: { 'application/json': { schema: ItemUpdateSchema } } },
  },
  responses: {
    200: {
      description: 'アイテム更新成功',
      content: { 'application/json': { schema: ItemSchema } },
    },
    404: { description: 'アイテムが存在しない' },
  },
})

export const deleteItemsItemIdRoute = createRoute({
  tags: [],
  method: 'delete',
  path: '/items/{itemId}',
  summary: '指定したIDのアイテムを削除',
  responses: {
    204: { description: 'アイテム削除成功(内容なし)' },
    404: { description: 'アイテムが存在しない' },
  },
})

src/index.ts

import { OpenAPIHono } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
import {
  getItemsRoute,
  postItemsRoute,
  parametersItemsItemIdRoute,
  getItemsItemIdRoute,
  putItemsItemIdRoute,
  deleteItemsItemIdRoute,
} from './routes.ts'
import {
  getItemsRouteHandler,
  postItemsRouteHandler,
  parametersItemsItemIdRouteHandler,
  getItemsItemIdRouteHandler,
  putItemsItemIdRouteHandler,
  deleteItemsItemIdRouteHandler,
} from './handler/items_handler.ts'

const app = new OpenAPIHono().basePath('api')

export const api = app
  .openapi(getItemsRoute, getItemsRouteHandler)
  .openapi(postItemsRoute, postItemsRouteHandler)
  .openapi(parametersItemsItemIdRoute, parametersItemsItemIdRouteHandler)
  .openapi(getItemsItemIdRoute, getItemsItemIdRouteHandler)
  .openapi(putItemsItemIdRoute, putItemsItemIdRouteHandler)
  .openapi(deleteItemsItemIdRoute, deleteItemsItemIdRouteHandler)

const isDev = process.env.NODE_ENV === 'development'

if (isDev) {
  app
    .doc('/doc', {
      openapi: '3.0.3',
      info: {
        title: 'Sample Complex REST API',
        version: '1.0.0',
        description:
          'このAPIは、複雑なスキーマ定義(入れ子になったオブジェクト、配列、条件付き型など)を含むサンプルのREST APIです。\n',
      },
      servers: [{ url: 'https://api.example.com/v1' }],
    })
    .get('/ui', swaggerUI({ url: '/api/doc' }))
}

export type AddType = typeof api

export default app

 その他、handlerなども生成されますが、省略します。

その他

  • 今後、これ以上機能追加の予定はありません。

  • hono-takibi.jsonの設定の仕様を、頻繁に変えてきましたが、0.5.0以降変更するつもりはありません。

  • オプションはより使いやすくなるように、変更する可能性はあります。

おわりに

0.5.0以降では、オプションでOpenAPI定義から、雛形を生成できるようにしました。また、zodスキーマの生成も、0.4.0から、対応箇所が増えました。(.length ...など)

 今後も、メンテナンスを継続し、精度が高いライブラリを目指します。

 また、Hono Takibiを試みた記事やブログをお待ちしております。

参考

Discussion