Hono Takibi というツールを作りました
Hono Takibi
はじめに
Hono Advent Calendar 2024の最終日の記事です。OpenAPI
定義から、Zod OpenAPI Hono
のコードを生成するツール、Hono Takibi
を紹介します。
OpenAPI Specification(OAS)
は、プログラミング言語に依存しないREST API
記述フォーマットです。以前はSwagger Specification
と呼ばれていました。
⚠️ 重要な注意点
Hono Takibi
を使用するためには、OpenAPI
定義(YAML
またはJSON
ファイル)が必要です。既存のAPIをZod OpenAPI Hono
に移行する場合は、まずOpenAPI
定義を用意する必要があります。OpenAPI
定義がない場合は、以下のような方法で作成できます。新規作成
Swagger Editor
などのツールを使用して新規作成- 生成AIを活用して
OpenAPI
定義を作成既存APIの移行
FastAPI
の自動生成されるOpenAPI
定義NestJS
の@nestjs/swagger
で生成される定義Spring Boot
のspringdoc-openapi
による定義
参考
Hono Takibiとは?
Hono Takibiは、OpenAPI
定義から、Zod OpenAPI Honoのコードを生成するツールです。開発者がビジネスロジックの実装に集中できるよう、定型的なコード生成を自動化することを目的としています。
Hono Takibiを作る上で参考にしたもの
Zodios
は、型安全なAPIクライアントを作成できます。そして、openapi-zod-client
、OpenAPI
定義からZodiosのコードを生成できます。
Hono
では、RPCという機能があり、Zodios
を使用せずに、型安全なAPIクライアントを実現可能です。
なぜHono Takibiを作ったのか?
もしかして、openapi-zod-client
のZod OpenAPI Hono
版があったりするのか?
もし、あれば、「OpenAPI定義さえあれば、Hono移行できるはず」
探したところ、なさそうなので、Hono Takibi
を作りました。
Hono Takibiという命名について
hono-takibi
という名前にしました。
最初は以下のどれかにしようと思っていました。
- hono-openapi-codegen
- openapi-to-hono
- oas-to-hono
- hono-oas-gen
- hono-gen
名前を募集したら、hono-takibi
という案が出たので、使わせていただきました。
開発者の負担軽減
私自身、命名に苦労しているので、変数名やスキーマ名の自動生成があればと思いました。
REST API
には、制約があります。
-
REST API
を設計する際には、各リソース一意なパスで識別しなければいけない -
APIの中心にあるのはデータであり、APIを作っているのは、リソース、パラメータ、レスポンス、それらのプロパティ
-
APIの設計は、一貫性のある名前を選ぶことから始まる
都合が良いことに、OpenAPI
は、REST API
記述フォーマットです。変数名が一意に定まります
Hono Takibiの機能
hono-takibi
は、OpenAPI
定義から、Zod OpenAPI Hono
のコードを生成します。
Zodスキーマの生成
OpenAPI
定義のcomponents
のschemas
に、Error
とPost
を定義します。
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
キャメルケース形式の変数名と、Zod
スキーマが生成されます。
現在、キャメルケース形式の変数生成のみが対応しています。また、Zod
スキーマの生成については、まだ完全には対応できていません。
const errorSchema = z.object({ message: z.string() })
const postSchema = z.object({
id: z.string().uuid(),
post: z.string().min(1).max(140),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
export const schemas = {
errorSchema,
postSchema,
}
ルート定義の生成
投稿APIの例を以下に示します。
paths:
/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.
変数名は、メソッド
+パス
+Route
で、postPostsRoute
となります。
export const postPostsRoute = createRoute({
tags: ['Post'],
method: 'post',
path: '/posts',
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 } },
},
},
})
Hono Takibiの使い方
hono-takibi
をインストールします。
npm add -D hono-takibi
OpenAPI
定義、yaml
またはjson
ファイルと、出力先のパスを指定して、hono-takibi
を実行します。
npx hono-takibi path/to/openapi.yaml -o path/to/output_hono.ts
Example
以下のような、OpenAPI
定義ファイルを用意します。
hono-rest-example.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: false
schema:
type: integer
minimum: 0
default: 1
description: The page number to retrieve. Must be a positive integer. Defaults to 1.
- in: query
name: rows
required: false
schema:
type: integer
minimum: 0
default: 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
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.
hono-takibi
を実行します。
npx hono-takibi example/hono-rest-example.yaml -o routes/index.ts
以下のような、routes/index.ts
が生成されます。
import { createRoute, z } from '@hono/zod-openapi'
const errorSchema = z.object({ message: z.string() })
const postSchema = z.object({
id: z.string().uuid(),
post: z.string().min(1).max(140),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
export const schemas = {
errorSchema,
postSchema,
}
export const getRoute = createRoute({
tags: ['Hono'],
method: 'get',
path: '/',
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() }) } },
},
},
})
export const postPostsRoute = createRoute({
tags: ['Post'],
method: 'post',
path: '/posts',
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',
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)).optional(),
rows: z.string().pipe(z.coerce.number().int().min(0)).optional(),
}),
},
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}',
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}',
description: 'Delete an existing post identified by its unique ID.',
request: { params: z.object({ id: z.string().uuid() }) },
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 } },
},
},
})
REST APIの作成
hono-takibi
のできるところは、ここまでです。実装は、開発者が行います。
ディレクトリ構造
.
├── prisma
│ ├── migrations
│ └── schema.prisma
└── src
├── handler
│ ├── hono_handler.ts
│ └── posts_handler.ts
├── index.ts
├── infra
│ └── index.ts
├── openapi
│ └── index.ts
└── service
└── posts_service.ts
Handler
hono_handler.ts
import type { RouteHandler } from '@hono/zod-openapi'
import type { getRoute } from '../openapi/index.js'
export const getHandler: RouteHandler<typeof getRoute> = async (c) => {
return c.json({ message: 'Hono🔥' }, 200)
}
posts_handler.ts
import type { RouteHandler } from '@hono/zod-openapi'
import type {
deletePostsIdRoute,
getPostsRoute,
postPostsRoute,
putPostsIdRoute,
} from '../openapi/index.js'
import { deletePostsId, getPosts, postPosts, putPostsId } from '../service/posts_service.js'
import type { Post } from '@prisma/client'
export const postPostsRouteHandler: RouteHandler<typeof postPostsRoute> = async (c) => {
const { post } = c.req.valid('json')
await postPosts(post)
return c.json({ message: 'Created' }, 201)
}
export const getPostsRouteHandler: RouteHandler<typeof getPostsRoute> = async (c) => {
const { page = 1, rows = 10 } = c.req.valid('query')
const limit = rows ?? 10
const offset = (page - 1) * rows
const posts: Post[] = await getPosts(limit, 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 putPostsId(id, post)
return new Response(null, { status: 204 })
}
export const deletePostsIdRouteHandler: RouteHandler<typeof deletePostsIdRoute> = async (c) => {
const { id } = c.req.valid('param')
await deletePostsId(id)
return new Response(null, { status: 204 })
}
Service
model Post {
id String @id @default(uuid())
post String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Prisma
というORMを使用して、操作を行います。
import type { Post } from '@prisma/client'
import prisma from '../infra/index.js'
export async function postPosts(post: string): Promise<Post> {
return await prisma.post.create({
data: {
post,
},
})
}
export async function getPosts(limit: number, offset: number): Promise<Post[]> {
return await prisma.post.findMany({
take: limit,
skip: offset,
orderBy: {
createdAt: 'desc',
},
})
}
export async function putPostsId(id: string, post: string): Promise<Post> {
return await prisma.post.update({
where: { id },
data: { post },
})
}
export async function deletePostsId(id: string): Promise<Post> {
return await prisma.post.delete({
where: { id },
})
}
hono-takibi
の場合、OpenAPI
の定義から、コードを生成します。
開発しながら、OpenAPI
の定義を作成していくことも可能です。
import { serve } from '@hono/node-server'
import { OpenAPIHono } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
import { apiReference } from '@scalar/hono-api-reference'
import {
deletePostsIdRoute,
getPostsRoute,
getRoute,
postPostsRoute,
putPostsIdRoute,
} from './openapi/index.js'
import { getHandler } from './handler/hono_handler.js'
import {
deletePostsIdRouteHandler,
getPostsRouteHandler,
postPostsRouteHandler,
putPostsIdRouteHandler,
} from './handler/posts_handler.js'
const app = new OpenAPIHono()
const api = app
.openapi(getRoute, getHandler)
.openapi(postPostsRoute, postPostsRouteHandler)
.openapi(getPostsRoute, getPostsRouteHandler)
.openapi(putPostsIdRoute, putPostsIdRouteHandler)
.openapi(deletePostsIdRoute, deletePostsIdRouteHandler)
api.use('*', async (c, next) => {
try {
await next()
} catch (e) {
return c.json({ error: (e as Error).message }, 500)
}
})
// swagger
app
.doc('/doc', {
info: {
title: 'Hono Sample API',
version: 'v1',
},
openapi: '3.0.0',
tags: [
{
name: 'Hono',
description: 'Hono API',
},
{
name: 'Post',
description: 'Post API',
},
],
})
.get('/ui', swaggerUI({ url: '/doc' }))
// scalar
app.get(
'/docs',
apiReference({
theme: 'saturn',
spec: {
url: '/doc',
},
}),
)
const port = 3000
console.log(`Server is running on http://localhost:${port}`)
serve({
fetch: app.fetch,
port,
})
export default api
swagger
scalar
期待できる効果
以下のようなケースで特に効果を発揮することを期待して、作成しました。
-
レガシーシステムの移行
- RubyやPHP、Perlなどで開発された既存APIのモダナイズ
- OpenAPI/Swagger定義が存在する古いRESTful APIのリプレース
-
新規開発の効率化
- APIファーストな開発アプローチにおける実装工数の削減
- 型安全性を担保したAPIの素早い実装
-
開発者の負担軽減
- 変数名の自動生成
- Zodスキーマの自動生成
- ルート定義の自動生成
生成AIと組み合わせる
生成AIにOpenAPI
定義を作成させ、hono-takibi
でコードを生成することも可能です。ただし、ビジネスロジックは自動生成の対象外となるため、開発者が実装する必要があります。
プロンプト
あなたは「REST APIのOpenAPI仕様」を生成する専門家です。以下の要件をすべて満たすOpenAPIドキュメントを YAML形式 で作成してください。なお、OpenAPIのバージョンは3.0以上 とし、誤りや不足がないよう注意深く出力してください。
generated.yaml
openapi: 3.0.3
info:
title: Todo API
description: これはシンプルなToDoリストを管理するためのAPIです。
version: 1.0.0
servers:
- url: https://api.example.com/v1
description: 本番サーバー
tags:
- name: Todos
description: Todoアイテムに関する操作
paths:
/todos:
get:
tags:
- Todos
summary: Todo一覧の取得
description: 登録されているTodoをすべて取得します。
operationId: getAllTodos
responses:
'200':
description: Todoの配列が返却されます。
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Todo'
'500':
description: サーバーエラー
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
post:
tags:
- Todos
summary: Todoの新規作成
description: Todoを新しく登録します。
operationId: createTodo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoCreateRequest'
examples:
validRequest:
summary: バリデーションを通るリクエスト例
value:
title: "買い物へ行く"
description: "牛乳、パン、卵を購入"
completed: false
responses:
'201':
description: 新規作成されたTodoを返却します。
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'400':
description: リクエスト不正
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: サーバーエラー
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/todos/{todoId}:
get:
tags:
- Todos
summary: Todoの取得
description: 指定したIDのTodoを取得します。
operationId: getTodoById
parameters:
- name: todoId
in: path
description: TodoのID
required: true
schema:
type: string
responses:
'200':
description: Todoオブジェクトを返却します。
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'404':
description: 指定したIDのTodoが存在しません。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: サーバーエラー
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
put:
tags:
- Todos
summary: Todoの更新
description: 指定したIDのTodoを更新します。
operationId: updateTodo
parameters:
- name: todoId
in: path
description: TodoのID
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoUpdateRequest'
examples:
validRequest:
summary: バリデーションを通る更新用リクエスト例
value:
title: "買い物リスト更新"
description: "牛乳、パン、卵、チーズを購入"
completed: false
responses:
'200':
description: 更新されたTodoを返却します。
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'400':
description: リクエスト不正
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: 指定したIDのTodoが存在しません。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: サーバーエラー
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
delete:
tags:
- Todos
summary: Todoの削除
description: 指定したIDのTodoを削除します。
operationId: deleteTodo
parameters:
- name: todoId
in: path
description: TodoのID
required: true
schema:
type: string
responses:
'204':
description: 削除に成功しました(レスポンスボディはありません)。
'404':
description: 指定したIDのTodoが存在しません。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: サーバーエラー
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
schemas:
Todo:
type: object
properties:
id:
type: string
example: "123e4567-e89b-12d3-a456-426614174000"
title:
type: string
example: "買い物へ行く"
description:
type: string
example: "牛乳、パン、卵を購入"
completed:
type: boolean
example: false
createdAt:
type: string
format: date-time
example: "2024-01-01T12:00:00Z"
updatedAt:
type: string
format: date-time
example: "2024-01-02T09:15:00Z"
required:
- id
- title
TodoCreateRequest:
type: object
properties:
title:
type: string
example: "買い物へ行く"
description:
type: string
example: "牛乳、パン、卵を購入"
completed:
type: boolean
default: false
required:
- title
TodoUpdateRequest:
type: object
properties:
title:
type: string
description:
type: string
completed:
type: boolean
required:
- title
ErrorResponse:
type: object
properties:
code:
type: integer
example: 400
message:
type: string
example: "Invalid request"
required:
- code
- message
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- BearerAuth: []
hono-takibi
で、生成させてみます。
import { createRoute, z } from '@hono/zod-openapi'
const todoSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
completed: z.boolean().optional(),
createdAt: z.string().datetime().optional(),
updatedAt: z.string().datetime().optional(),
})
const todoCreateRequestSchema = z.object({
title: z.string(),
description: z.string().optional(),
completed: z.boolean().optional(),
})
const todoUpdateRequestSchema = z.object({
title: z.string(),
description: z.string().optional(),
completed: z.boolean().optional(),
})
const errorResponseSchema = z.object({ code: z.number().int(), message: z.string() })
export const schemas = {
todoSchema,
todoCreateRequestSchema,
todoUpdateRequestSchema,
errorResponseSchema,
}
export const getTodosRoute = createRoute({
tags: ['Todos'],
method: 'get',
path: '/todos',
description: '登録されているTodoをすべて取得します。',
responses: {
200: {
description: 'Todoの配列が返却されます。',
content: { 'application/json': { schema: z.array(todoSchema) } },
},
500: {
description: 'サーバーエラー',
content: { 'application/json': { schema: errorResponseSchema } },
},
},
})
export const postTodosRoute = createRoute({
tags: ['Todos'],
method: 'post',
path: '/todos',
description: 'Todoを新しく登録します。',
request: {
body: { required: true, content: { 'application/json': { schema: todoCreateRequestSchema } } },
},
responses: {
201: {
description: '新規作成されたTodoを返却します。',
content: { 'application/json': { schema: todoSchema } },
},
400: {
description: 'リクエスト不正',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'サーバーエラー',
content: { 'application/json': { schema: errorResponseSchema } },
},
},
})
export const getTodosTodoIdRoute = createRoute({
tags: ['Todos'],
method: 'get',
path: '/todos/{todoId}',
description: '指定したIDのTodoを取得します。',
request: { params: z.object({ todoId: z.string() }) },
responses: {
200: {
description: 'Todoオブジェクトを返却します。',
content: { 'application/json': { schema: todoSchema } },
},
404: {
description: '指定したIDのTodoが存在しません。',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'サーバーエラー',
content: { 'application/json': { schema: errorResponseSchema } },
},
},
})
export const putTodosTodoIdRoute = createRoute({
tags: ['Todos'],
method: 'put',
path: '/todos/{todoId}',
description: '指定したIDのTodoを更新します。',
request: {
body: { required: true, content: { 'application/json': { schema: todoUpdateRequestSchema } } },
params: z.object({ todoId: z.string() }),
},
responses: {
200: {
description: '更新されたTodoを返却します。',
content: { 'application/json': { schema: todoSchema } },
},
400: {
description: 'リクエスト不正',
content: { 'application/json': { schema: errorResponseSchema } },
},
404: {
description: '指定したIDのTodoが存在しません。',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'サーバーエラー',
content: { 'application/json': { schema: errorResponseSchema } },
},
},
})
export const deleteTodosTodoIdRoute = createRoute({
tags: ['Todos'],
method: 'delete',
path: '/todos/{todoId}',
description: '指定したIDのTodoを削除します。',
request: { params: z.object({ todoId: z.string() }) },
responses: {
204: { description: '削除に成功しました(レスポンスボディはありません)。' },
404: {
description: '指定したIDのTodoが存在しません。',
content: { 'application/json': { schema: errorResponseSchema } },
},
500: {
description: 'サーバーエラー',
content: { 'application/json': { schema: errorResponseSchema } },
},
},
})
Hono Takibiの目指すもの
レガシーからモダンへの橋渡し
OpenAPI
が言語に依存しない仕様であることは、レガシーシステムのモダン化において以下の重要な価値を提供すると考えています。
-
言語非依存
- YAML/JSONベースの記述
- 実装言語に縛られない設計
- 普遍的なAPI表現
-
資産としての価値
- 既存APIの資産を活かす
-
移行の容易性
- 既存の仕様を活かす
- 段階的な移行を可能にする
- 新旧システムの共存期間における一貫性の維持
-
開発者の負担軽減
- 変数名の自動生成
- Zodスキーマの自動生成
- ルート定義の自動生成
- 自動生成による実装工数の削減
- 型安全性による品質の担保
- ビジネスロジックへの集中が可能
- 生成AIによる作成された
OpenAPI
定義を、hono-takibi
でコード生成
その他
Hono
で、開発しながらOpenAPI
定義を作成することも可能です。最近、Valibot
が、サポートされはじめ、Zod
ではなく、Valibot
を使用することも可能です。
Hono OpenAPIのアプローチ
Zod OpenAPI Honoのアプローチ
Hono Takibiのアプローチ
おわりに
Hono Takibi
は、まだ発展途上のツールです。以下のような課題や改善点があります。
現在の制限事項
-
キャメルケースのみの変数名生成
-
一部のZodスキーマ生成に未対応
-
その他
Hono Takibi
の今後について考えていること
-
型定義の自動生成機能として、
z.infer()
を使用したコード生成の追加を検討中 -
シンプルさを保つため、多機能すぎるツールにはせず、必要最小限の機能に絞る予定
-
メンテナンス性を考慮し、新たな外部ライブラリへの依存は最小限に抑える方針
より良いツールにしていくため、フィードバックやコントリビューションをお待ちしています。GitHubでのIssue報告や機能改善の提案、プルリクエストを歓迎します。
私自身、開発経験が浅く、現在のソースコードにはコメントを多く残している段階です。経験豊富な開発者の方々からのアドバイスをいただけると幸いです。
Discussion