Hono Takibi 0.4.0
Hono Takibi 0.4.0
npm
GitHub
Hono Takibi 0.4.0の変更点
-
スキーマ名の命名規則は、defaultでは
PascalCase
となりました。 -
appオプションを追加し、定型的なコード生成をサポートしました。
Hono-Takibi 設定ガイド
プロジェクトのルートディレクトリに hono-takibi.json
ファイルを作成することで、コード生成の動作をカスタマイズできます。
スキーマオプション(Schema Options)
オプション | 型 | デフォルト | 説明 |
---|---|---|---|
name |
"PascalCase" | "camelCase"
|
"PascalCase" |
生成されるスキーマ変数の命名規則 |
export |
boolean |
false |
true にすると、すべてのスキーマ定義をエクスポート |
型オプション(Type Options)
オプション | 型 | デフォルト | 説明 |
---|---|---|---|
name |
"PascalCase" | "camelCase"
|
"PascalCase" |
生成される型定義の命名規則 |
export |
boolean |
false |
true にすると、すべての型定義をエクスポート |
アプリオプション(App Options)
オプション | 型 | デフォルト | 説明 |
---|---|---|---|
output |
boolean |
false |
アプリケーションおよびハンドラーファイルの生成を制御 true にすると、メインのアプリケーションファイルと対応するルートハンドラーを作成 |
test |
boolean |
false |
API エンドポイントのテストファイルを自動生成 |
basePath |
string |
"" |
API エンドポイントのベースURLパスを指定 |
isDev |
string |
"process.env.NODE_ENV" |
開発モードを判定する環境変数を指定(Swagger UI の有効化などに影響) |
入力と出力(Input and Output)
入力と出力のパスは、次の2つの方法で指定できます。
1. コマンドライン引数
hono-takibi.json
)
2. 設定ファイル(オプション | 型 | デフォルト | 説明 |
---|---|---|---|
input |
string |
"" |
入力ファイルのパス |
output |
string |
"" |
出力ファイルのパス |
⚠️ 注意: 設定ファイルを使用する場合、コマンドライン引数は不要です。
設定ファイルの設定がコマンドライン引数よりも優先されます。
hono-takibi.json
を設定したら、次のコマンドだけで実行できます:
npx hono-takibi
hono-takibi.json
のサンプル
デフォルトの動作(スキーマと型定義の命名は、PascalCase
)
{
"input": "src/openapi/openapi.yaml",
"output": "src/openapi/index.ts",
"schema": {
"name": "PascalCase",
"export": false
},
"type": {
"name": "PascalCase",
"export": false
},
"app": {
"output": true
}
}
Hono Takibi0.4.0の新機能
以下のような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.
このopenapi.yaml
を用いて、hono-takibi.json
を以下のように設定します。
{
"input": "src/openapi/openapi.yaml",
"output": "src/openapi/index.ts",
"schema": {
"name": "PascalCase",
"export": false
},
"type": {
"name": "PascalCase",
"export": false
},
"app": {
"output": true
}
}
以下のようなディレクトリ構成にします。
.
├── hono-takibi.json
├── package.json
├── src
│ ├── index.ts
│ └── openapi
│ └── openapi.yaml
└── tsconfig.json
Hono Takibiの実行
npx hono-takibi
生成されたファイル
./index.ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
import {
getRoute,
postPostsRoute,
getPostsRoute,
putPostsIdRoute,
deletePostsIdRoute,
} from './src/openapi'
import { getRouteHandler } from './src/openapi/handler/index_handler.ts'
import {
postPostsRouteHandler,
getPostsRouteHandler,
putPostsIdRouteHandler,
deletePostsIdRouteHandler,
} from './src/openapi/handler/posts_handler.ts'
const app = new OpenAPIHono()
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: '/doc' }))
}
export type AddType = typeof api
export default app
./src/openapi/index.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/openapi/handler/index_handler.ts
import type { RouteHandler } from '@hono/zod-openapi'
import type { getRoute } from '../index.ts'
export const getRouteHandler: RouteHandler<typeof getRoute> = async (c) => {}
./src/openapi/handler/posts_handler.ts
import type { RouteHandler } from '@hono/zod-openapi'
import type {
postPostsRoute,
getPostsRoute,
putPostsIdRoute,
deletePostsIdRoute,
} from '../index.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) => {}
ディレクトリ構成は以下のようになります。
.
├── hono-takibi.json
├── index.ts
├── package.json
├── src
│ ├── index.ts
│ └── openapi
│ ├── handler
│ │ ├── index_handler.ts
│ │ └── posts_handler.ts
│ ├── index.ts
│ └── openapi.yaml
└── tsconfig.json
OpenAPI
定義から、雛形を生成できるようになりました。
残りは、./src/index.ts
の設定と、ビジネスロジックの実装のみです。
import { serve } from '@hono/node-server'
import app from '..'
const port = 3000
console.log(`Server is running on http://localhost:${port}`)
serve({
fetch: app.fetch,
port,
})
Viteとの組み合わせ
これは、実験的な機能です。他のPluginとの組み合わせで、動作しない可能性があります。
-
Vite
が、立ち上がっている間、OpenAPI
の定義を編集すると、ルート定義やスキーマも更新されるという機能です。
import { defineConfig } from 'vite'
import honoTakibiPlugin from 'hono-takibi/vite-plugin'
export default defineConfig({
plugins: [
honoTakibiPlugin({
input: 'src/openapi/openapi.yaml',
output: 'src/openapi/index.ts',
packageManager: 'npm',
}),
],
})
⚠️ 注意:
0.4.1
以降では、以下のように設定してください。他のPluginとの組み合わせで、動作しない可能性は解決していません。import { defineConfig } from 'vite' import honoTakibiPlugin from 'hono-takibi/vite-plugin' export default defineConfig({ plugins: [honoTakibiPlugin()], })
Demo
おわりに
Hono Takibi
0.4.0の新機能を紹介しました。実験的な機能もあるため、使用には注意が必要です。
また、趣味で開発しているため、バグや不具合、未実装があるかもしれません。その場合は、GitHubでIssueを報告してください。
より良いツールにしていくため、フィードバックやコントリビューションをお待ちしています。
Discussion