🔥

スキーマ駆動で、Zod OpenAPI Honoによる、API開発するために、Hono Takibiというライブラリを作っている

に公開

Hono Takibi

はじめに

Hono Conference 2025で、Hono Takibiについて紹介しました。

https://hono.connpass.com/event/368214/

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

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

https://fortee.jp/honoconf-2025/proposal/5d30e23c-6821-487e-98db-664138be6edf

スライド

https://speakerdeck.com/nakita628/welcome-to-slidev-slidev

Hono Takibiの紹介

Hono Takibiは、OpenAPI定義から、Zod OpenAPI Honoのコードを生成するツールです。

ディレクトリ構成

.
├── main.tsp
├── package.json
├── src
│   └── **
└── tsconfig.json

 今回の例では、Typespecを使用して紹介します。以下をinstallする必要があります。

import "@typespec/http";

using Http;

model Hono {
  value: "Hono"
}

model HonoX {
  value: "HonoX"
}
  
model ZodOpenAPIHono {
  value: "ZodOpenAPIHono"
}

@route("/hono")
@tag("Hono")
interface HonoService {
  @get get(): Hono
}
    
@route("/honox")
@tag("HonoX")
interface HonoXService {
  @get get(): HonoX
}

@route("/zod-openapi-hono")
@tag("ZodOpenAPIHono")
interface ZodOpenAPIHonoService {
  @get get(): ZodOpenAPIHono
}

hono-takibiを実行します。

npx hono-takibi main.tsp -o src/routes/index.ts

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

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

const HonoSchema = z.object({ value: z.literal('Hono') }).openapi('Hono')

const HonoXSchema = z.object({ value: z.literal('HonoX') }).openapi('HonoX')

const ZodOpenAPIHonoSchema = z
  .object({ value: z.literal('ZodOpenAPIHono') })
  .openapi('ZodOpenAPIHono')

export const getHonoRoute = createRoute({
  tags: ['Hono'],
  method: 'get',
  path: '/hono',
  operationId: 'HonoService_get',
  responses: {
    200: {
      description: 'The request has succeeded.',
      content: { 'application/json': { schema: HonoSchema } },
    },
  },
})

export const getHonoxRoute = createRoute({
  tags: ['HonoX'],
  method: 'get',
  path: '/honox',
  operationId: 'HonoXService_get',
  responses: {
    200: {
      description: 'The request has succeeded.',
      content: { 'application/json': { schema: HonoXSchema } },
    },
  },
})

export const getZodOpenapiHonoRoute = createRoute({
  tags: ['ZodOpenAPIHono'],
  method: 'get',
  path: '/zod-openapi-hono',
  operationId: 'ZodOpenAPIHonoService_get',
  responses: {
    200: {
      description: 'The request has succeeded.',
      content: { 'application/json': { schema: ZodOpenAPIHonoSchema } },
    },
  },
})

出力のカスタマイズ

hono-takibi.config.tsを用意します。

import { defineConfig } from 'hono-takibi/config'

export default defineConfig({
  input: 'main.tsp',
  'zod-openapi': {
    schema: { output: 'src/schemas/index.ts' },
    route: { output: 'src/routes/index.ts', import: '../schemas' },
  },
})

 必ず、プロジェクトルートに配置してください。

.
├── hono-takibi.config.ts
├── main.tsp
├── package.json
├── package-lock.json
├── src
│   └── **
└── tsconfig.json

hono-takibiを実行します。

npx hono-takibi

 以下のように、ファイルが生成されます。

.
├── hono-takibi.config.ts
├── main.tsp
├── package.json
├── package-lock.json
├── src
│   ├── routes
│   │   └── index.ts
│   └── schemas
│       └── index.ts
└── tsconfig.json

 src/routes/index.ts

import { createRoute } from '@hono/zod-openapi'
import { HonoSchema, HonoXSchema, ZodOpenAPIHonoSchema } from '../schemas'

export const getHonoRoute = createRoute({
  tags: ['Hono'],
  method: 'get',
  path: '/hono',
  operationId: 'HonoService_get',
  responses: {
    200: {
      description: 'The request has succeeded.',
      content: { 'application/json': { schema: HonoSchema } },
    },
  },
})

export const getHonoxRoute = createRoute({
  tags: ['HonoX'],
  method: 'get',
  path: '/honox',
  operationId: 'HonoXService_get',
  responses: {
    200: {
      description: 'The request has succeeded.',
      content: { 'application/json': { schema: HonoXSchema } },
    },
  },
})

export const getZodOpenapiHonoRoute = createRoute({
  tags: ['ZodOpenAPIHono'],
  method: 'get',
  path: '/zod-openapi-hono',
  operationId: 'ZodOpenAPIHonoService_get',
  responses: {
    200: {
      description: 'The request has succeeded.',
      content: { 'application/json': { schema: ZodOpenAPIHonoSchema } },
    },
  },
})

 src/schemas/index.ts

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

export const HonoSchema = z.object({ value: z.literal('Hono') }).openapi('Hono')

export const HonoXSchema = z.object({ value: z.literal('HonoX') }).openapi('HonoX')

export const ZodOpenAPIHonoSchema = z
  .object({ value: z.literal('ZodOpenAPIHono') })
  .openapi('ZodOpenAPIHono')

 さらに分割したい場合は、以下のようになります。split: trueを追加し、outputをディレクトリ名にすることで、分割されます。

import { defineConfig } from 'hono-takibi/config'

export default defineConfig({
  input: 'main.tsp',
  'zod-openapi': {
    schema: { output: 'src/schemas', split: true },
    route: { output: 'src/routes', import: '../schemas', split: true },
  },
})

 以下のように、1ファイル1ルート、1ファイル1スキーマになります。

.
├── hono-takibi.config.ts
├── main.tsp
├── package.json
├── package-lock.json
├── src
│   ├── routes
│   │   ├── getHono.ts
│   │   ├── getHonox.ts
│   │   ├── getZodOpenapiHono.ts
│   │   └── index.ts
│   └── schemas
│       ├── hono.ts
│       ├── honoX.ts
│       ├── index.ts
│       └── zodOpenAPIHono.ts
└── tsconfig.json

 src/routes/index.ts

export * from './getHono'
export * from './getHonox'
export * from './getZodOpenapiHono'

 src/routes/getHono.ts

import { createRoute } from '@hono/zod-openapi'
import { HonoSchema } from '../schemas'

export const getHonoRoute = createRoute({
  tags: ['Hono'],
  method: 'get',
  path: '/hono',
  operationId: 'HonoService_get',
  responses: {
    200: {
      description: 'The request has succeeded.',
      content: { 'application/json': { schema: HonoSchema } },
    },
  },
})

 他のルートファイルも同様です。

 src/schemas/index.ts

export * from './hono'
export * from './honoX'
export * from './zodOpenAPIHono'

 src/schemas/hono.ts

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

export const HonoSchema = z.object({ value: z.literal('Hono') }).openapi('Hono')

 他のスキーマファイルも同様です。

RPCの生成

clientのパスをimportで合わせる必要があります。

type AddType = typeof api

export const client = hc<AddType>('/')

 以下のように設定します。

import { defineConfig } from 'hono-takibi/config'

export default defineConfig({
  input: 'main.tsp',
  rpc: {
    output: 'src/rpc/index.ts',
    import: '../index',
  },
})

 src/rpc/index.ts

import { client } from '../index'

/**
 * GET /hono
 */
export async function getHono() {
  return await client.hono.$get()
}

/**
 * GET /honox
 */
export async function getHonox() {
  return await client.honox.$get()
}

/**
 * GET /zod-openapi-hono
 */
export async function getZodOpenapiHono() {
  return await client['zod-openapi-hono'].$get()
}

split: trueにし、分割も可能です。

import { defineConfig } from 'hono-takibi/config'

export default defineConfig({
  input: 'main.tsp',
  rpc: {
    output: 'src/rpc',
    import: '../index',
    split: true,
  },
})

 以下のように、1ファイル1RPCになります。

.
├── hono-takibi.config.ts
├── main.tsp
├── package.json
├── package-lock.json
├── src
│   ├── handlers
│   │   └── **
│   ├── index.ts
│   ├── routes
│   │   └── **
│   ├── rpc
│   │   ├── getHono.ts
│   │   ├── getHonox.ts
│   │   ├── getZodOpenapiHono.ts
│   │   └── index.ts
│   └── schemas
│       └── **
└── tsconfig.json

Viteのと組み合わせ

hono-takibi.config.tsの設定が利用されます。Vite起動時、API定義を編集すると、コードが自動で更新されます。

import { honoTakibiVite } from 'hono-takibi/vite-plugin'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [honoTakibiVite()],
})

Demo

おわりに

Hono Conference 2025の発表内容の記事でした。ぜひ、hono-takibiを試してみてください。

参考リンク

Discussion