hono-takibi
v0.9.9997
Published
Hono Takibi is a code generator from OpenAPI to @hono/zod-openapi
Readme
![]()
Hono Takibi

npm install -D hono-takibiOpenAPI to Hono Code Generator
Hono Takibi generates type-safe Hono code from OpenAPI / TypeSpec specifications.
- OpenAPI schemas to Zod schemas
- @hono/zod-openapi route definitions
- App entry point + handler stubs + test files
- Client library hooks (SWR, TanStack Query, Preact Query, Solid Query, Vue Query, Svelte Query, Angular Query)
- RPC client, mock server, TypeScript types
- API reference docs with hono-cli commands
Quick Start
CLI
npx hono-takibi path/to/input.{yaml,json,tsp} -o path/to/output.tsConfiguration File
Create hono-takibi.config.ts:
import { defineConfig } from 'hono-takibi/config'
export default defineConfig({
input: 'openapi.yaml',
'zod-openapi': {
output: './src/routes.ts',
},
})npx hono-takibiExample
input:
openapi: 3.1.0
info:
title: Hono Takibi API
version: '1.0.0'
paths:
/:
get:
summary: Welcome
description: Returns a welcome message from Hono Takibi.
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: Hono Takibi🔥
required:
- messageoutput:
import { createRoute, z } from '@hono/zod-openapi'
export const getRoute = createRoute({
method: 'get',
path: '/',
summary: 'Welcome',
description: 'Returns a welcome message from Hono Takibi.',
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: z
.object({
message: z.string().openapi({ example: 'Hono Takibi🔥' }),
})
.openapi({ required: ['message'] }),
},
},
},
},
})Vite Plugin
Watches your OpenAPI spec and hono-takibi.config.ts for changes, then auto-regenerates code on save.
Requires hono-takibi.config.ts in your project root.
// vite.config.ts
import { honoTakibiVite } from 'hono-takibi/vite-plugin'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [honoTakibiVite()],
})
Template & Test Generation
Generate a complete app structure with handler stubs and test files:
export default defineConfig({
input: 'openapi.yaml',
'zod-openapi': {
output: './src/routes.ts',
template: {
test: true,
pathAlias: '@/',
testFramework: 'bun', // "vitest" (default) | "vite-plus" | "bun"
},
},
})This generates:
src/index.ts- App entry point with route registrationssrc/handlers/*.ts- Handler stubs for each resourcesrc/handlers/*.test.ts- Test files with@faker-js/fakermock data (imports fromvitest,vite-plus/test, orbun:test)
Re-running after updating your OpenAPI spec is safe — your hand-written handler logic and test customizations are preserved. Only new routes are added as stubs.
Handler Generation Modes
routeHandler: false (default)
Each handler creates its own sub-router and registers routes:
// src/handlers/health.ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { getHealthRoute } from '@/routes'
const app = new OpenAPIHono()
export const healthHandler = app.openapi(getHealthRoute, (c) => {})The app mounts handlers via .route():
// src/index.ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { healthHandler } from './handlers'
const app = new OpenAPIHono()
export const api = app.route('/', healthHandler)
export default approuteHandler: true
Handlers export typed RouteHandler functions, and index.ts centralizes route registration:
// src/handlers/health.ts
import type { RouteHandler } from '@hono/zod-openapi'
import type { getHealthRoute } from '../routes'
export const getHealthRouteHandler: RouteHandler<typeof getHealthRoute> = async (c) => {}// src/index.ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { getHealthRoute } from './routes'
import { getHealthRouteHandler } from './handlers'
const app = new OpenAPIHono()
export const api = app.openapi(getHealthRoute, getHealthRouteHandler)
export default appClient Library Integrations
Supported: SWR, TanStack Query, Preact Query, Solid Query, Vue Query, Svelte Query, Angular Query, RPC Client.
export default defineConfig({
input: 'openapi.yaml',
'tanstack-query': {
output: './src/tanstack-query',
import: '../client',
split: true,
client: 'client',
},
})Infinite Query (x-pagination)
Set x-pagination: true on a GET operation to generate infinite query hooks.
paths:
/posts:
get:
x-pagination: trueTest & Mock Generation
Test Generation
export default defineConfig({
input: 'openapi.yaml',
test: {
output: './src/test.ts',
import: '../index',
testFramework: 'bun', // "vitest" (default) | "vite-plus" | "bun"
},
})Mock Server Generation
export default defineConfig({
input: 'openapi.yaml',
mock: {
output: './src/mock.ts',
},
})API Reference Docs
Generate API reference Markdown with hono-cli hono request commands that can be run directly without starting a server:
export default defineConfig({
input: 'openapi.yaml',
docs: {
output: './docs/api.md',
entry: 'src/index.ts',
},
})To generate curl commands instead of hono request:
export default defineConfig({
input: 'openapi.yaml',
docs: {
output: './docs/api.md',
curl: true,
baseUrl: 'http://localhost:3000',
},
})Full Config Reference
// hono-takibi.config.ts
import { defineConfig } from 'hono-takibi/config'
export default defineConfig({
// OpenAPI spec file (.yaml, .json, or .tsp)
input: 'openapi.yaml',
// Base path prefix for all routes
basePath: '/api',
// oxfmt FormatConfig for generated code output
// @see https://www.npmjs.com/package/oxfmt
// format: {},
// Main code generation (Zod + OpenAPI + Hono)
'zod-openapi': {
// Output: use 'output' for single file, or 'routes' for split mode (mutually exclusive)
output: './src/routes.ts',
readonly: true, // Add 'as const' to generated schemas
// Template generation (app entry point + handler stubs + tests)
template: {
test: true, // Generate test files
routeHandler: false, // false: inline .openapi() (default), true: RouteHandler exports
pathAlias: '@/', // TypeScript path alias for imports
testFramework: 'vitest', // "vitest" (default) | "vite-plus" | "bun" — test import source
},
// Export options (OpenAPI Components Object)
exportSchemas: true,
exportSchemasTypes: true,
exportResponses: true,
exportParameters: true,
exportParametersTypes: true,
exportExamples: true,
exportRequestBodies: true,
exportHeaders: true,
exportHeadersTypes: true,
exportSecuritySchemes: true,
exportLinks: true,
exportCallbacks: true,
exportPathItems: true,
exportMediaTypes: true,
exportMediaTypesTypes: true,
// Split routes into separate files
routes: {
output: './src/routes',
split: true,
import: '@packages/routes',
},
// Split webhooks into separate files
webhooks: {
output: './src/webhooks',
split: true,
import: '@packages/webhooks',
},
// Split components into separate files
components: {
schemas: {
output: './src/schemas',
exportTypes: true,
split: true,
import: '../schemas',
},
responses: {
output: './src/responses',
split: true,
import: '../responses',
},
parameters: {
output: './src/parameters',
exportTypes: true,
split: true,
import: '../parameters',
},
examples: {
output: './src/examples',
split: true,
import: '../examples',
},
requestBodies: {
output: './src/requestBodies',
split: true,
import: '../requestBodies',
},
headers: {
output: './src/headers',
exportTypes: true,
split: true,
import: '../headers',
},
securitySchemes: {
output: './src/securitySchemes',
split: true,
import: '../securitySchemes',
},
links: {
output: './src/links',
split: true,
import: '../links',
},
callbacks: {
output: './src/callbacks',
split: true,
import: '../callbacks',
},
pathItems: {
output: './src/pathItems',
split: true,
import: '../pathItems',
},
mediaTypes: {
output: './src/mediaTypes',
exportTypes: true,
split: true,
import: '../mediaTypes',
},
},
},
// TypeScript type generation
type: {
output: './src/types.ts',
readonly: true,
},
// RPC client generation
rpc: {
output: './src/rpc',
import: '../client', // Import path for the Hono RPC client
split: true,
client: 'client', // Export name of the client instance
parseResponse: true, // Use parseResponse for type-safe responses
},
// Client library integrations (SWR, TanStack Query, Preact Query, Solid Query, Vue Query, Svelte Query, Angular Query)
swr: {
output: './src/swr',
import: '../client',
split: true,
client: 'client',
},
'tanstack-query': {
output: './src/tanstack-query',
import: '../client',
split: true,
client: 'client',
},
'preact-query': {
output: './src/preact-query',
import: '../client',
split: true,
client: 'client',
},
'solid-query': {
output: './src/solid-query',
import: '../client',
split: true,
client: 'client',
},
'vue-query': {
output: './src/vue-query',
import: '../client',
split: true,
client: 'client',
},
'svelte-query': {
output: './src/svelte-query',
import: '../client',
split: true,
client: 'client',
},
'angular-query': {
output: './src/angular-query',
import: '../client',
split: true,
client: 'client',
},
// Test generation
test: {
output: './src/test.ts',
import: '../index', // Import path for the app instance
testFramework: 'vitest', // "vitest" (default) | "vite-plus" | "bun" — test import source
},
// Mock server generation
mock: {
output: './src/mock.ts',
},
// API reference docs generation
docs: {
output: './docs/api.md',
entry: 'src/index.ts', // App entry point for hono request commands
curl: false, // true: generate curl commands (requires baseUrl), false: hono request (default)
baseUrl: 'http://localhost:3000', // Base URL for curl commands (required when curl: true)
},
})Custom Validation Error Messages
Use x-* vendor extensions to attach custom Zod error messages, with one extension per JSON Schema keyword (1:1 mapping). The extension name follows the pattern x-<jsonSchemaKeyword>-message (e.g. x-minLength-message, x-pattern-message), plus four generic forms: x-error-message, x-required-message, x-const-message, x-enum-message.
name:
type: string
minLength: 1
maxLength: 50
x-error-message: 'Name must be a string'
x-minLength-message: 'Name cannot be empty'
x-maxLength-message: 'Name must be at most 50 characters'z.string({ error: 'Name must be a string' })
.min(1, { error: 'Name cannot be empty' })
.max(50, { error: 'Name must be at most 50 characters' })Extension Reference
All custom message extensions follow the x-<keyword>-message naming convention and map directly to Zod validator error messages.
Common (any schema type)
| Extension | Applies to |
| -------------------- | -------------------------------------------------------------- |
| x-error-message | All schemas (fallback when keyword-specific message is absent) |
| x-required-message | Required properties |
| x-const-message | const |
| x-enum-message | enum |
Numeric (number / integer)
| Extension | Applies to |
| ---------------------------- | ------------------ |
| x-minimum-message | minimum |
| x-maximum-message | maximum |
| x-exclusiveMinimum-message | exclusiveMinimum |
| x-exclusiveMaximum-message | exclusiveMaximum |
| x-multipleOf-message | multipleOf |
String
| Extension | Applies to |
| --------------------- | ------------------------------------------ |
| x-minLength-message | minLength |
| x-maxLength-message | maxLength |
| x-pattern-message | pattern |
| x-length-message | Exact length (minLength === maxLength) |
Array
| Extension | Applies to |
| ----------------------- | ------------- |
| x-minItems-message | minItems |
| x-maxItems-message | maxItems |
| x-uniqueItems-message | uniqueItems |
| x-contains-message | contains |
| x-minContains-message | minContains |
| x-maxContains-message | maxContains |
Object
| Extension | Applies to |
| -------------------------------- | ---------------------- |
| x-minProperties-message | minProperties |
| x-maxProperties-message | maxProperties |
| x-additionalProperties-message | additionalProperties |
| x-propertyNames-message | propertyNames |
| x-patternProperties-message | patternProperties |
| x-dependentRequired-message | dependentRequired |
| x-dependentSchemas-message | dependentSchemas |
Combinators
| Extension | Applies to |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| x-allOf-message | allOf |
| x-anyOf-message | anyOf |
| x-oneOf-message | oneOf |
| x-not-message | not |
| x-implication-message | Implication pattern (A → B) encoded as anyOf:[{not:A},{required:B}]; takes precedence over x-anyOf-message |
Conditional
| Extension | Applies to |
| ---------------- | ---------- |
| x-if-message | if |
| x-then-message | then |
| x-else-message | else |
Typeless / Array Applicator
| Extension | Applies to |
| --------------------------------- | ------------------------------- |
| x-properties-message | properties (typeless schemas) |
| x-prefixItems-message | prefixItems |
| x-items-message | items |
| x-unevaluatedProperties-message | unevaluatedProperties |
| x-unevaluatedItems-message | unevaluatedItems |
Behavior Extensions
String Pre-validation Transforms
| Extension | Generated | Value |
| --------------- | ----------------------------- | --------------------------------------- |
| x-trim | z.string().trim() | true |
| x-toLowerCase | z.string().toLowerCase() | true |
| x-toUpperCase | z.string().toUpperCase() | true |
| x-normalize | z.string().normalize('NFC') | 'NFC' / 'NFD' / 'NFKC' / 'NFKD' |
homepage:
type: string
format: url
x-trim: truez.string().trim().pipe(z.url())String Validation Checks
| Extension | Generated | Value |
| ------------- | ------------------------ | ------ |
| x-lowercase | z.string().lowercase() | true |
| x-uppercase | z.string().uppercase() | true |
slug:
type: string
x-lowercase: truez.string().lowercase()Preprocess (Input Normalization)
x-preprocess
username:
type: string
x-preprocess: 'z.preprocess((val) => typeof val === "string" ? val.trim() : val, z.string())'z.preprocess((val) => (typeof val === 'string' ? val.trim() : val), z.string())Type Coercion
x-coerce
asNumber:
type: number
x-coerce: true
asDate:
type: string
format: date-time
x-coerce: truez.coerce.number()
z.coerce.date()x-stringbool
notify:
type: boolean
x-stringbool: truez.stringbool()notify:
type: boolean
x-stringbool:
truthy: ['yes', 'on']
falsy: ['no', 'off']
case: 'sensitive'z.stringbool({ truthy: ['yes', 'on'], falsy: ['no', 'off'], case: 'sensitive' })Codec (Bidirectional Transform)
x-codec
updatedAt:
type: string
format: date-time
x-codec: 'z.codec(z.iso.datetime(), z.date(), { decode: (val) => new Date(val), encode: (val) => val.toISOString() })'z.codec(z.iso.datetime(), z.date(), {
decode: (val) => new Date(val),
encode: (val) => val.toISOString(),
})Custom Validation
x-refine
password:
type: string
x-refine: '.refine((val) => val.length >= 8, { message: "Password must be at least 8 characters" }).refine((val) => /[A-Z]/.test(val), { message: "Password must contain an uppercase letter" })'z.string()
.refine((val) => val.length >= 8, { message: 'Password must be at least 8 characters' })
.refine((val) => /[A-Z]/.test(val), { message: 'Password must contain an uppercase letter' })x-superRefine
normalizedEmail:
type: string
format: email
x-superRefine: '.superRefine((val, ctx) => { if (val.endsWith("@blocked.example")) ctx.addIssue({ code: "custom", message: "Blocked domain" }) })'z.email().superRefine((val, ctx) => {
if (val.endsWith('@blocked.example')) {
ctx.addIssue({ code: 'custom', message: 'Blocked domain' })
}
})Transform & Pipe
x-transform
code:
type: string
x-transform: 'z.string().transform((val) => val.toUpperCase())'z.string().transform((val) => val.toUpperCase())x-pipe
port:
type: string
x-pipe: 'z.string().pipe(z.number().int().positive())'z.string().pipe(z.number().int().positive())Default & Fallback Values
x-prefault
greeting:
type: string
x-prefault: 'hello'z.string().prefault('hello')x-catch
retries:
type: integer
x-catch: 0z.int().catch(0)Immutability
x-readonly
config:
type: object
properties:
name:
type: string
x-readonly: truez.object({ name: z.string() }).readonly()String Content Checks
x-startsWith / x-endsWith / x-includes
url:
type: string
x-startsWith: 'https://'
x-endsWith: '.com'
path:
type: string
x-includes: '/api/'z.string().startsWith('https://').endsWith('.com')
z.string().includes('/api/')Format-Specific Options
htmlEmail:
type: string
format: email
x-emailPattern: 'html5' # email pattern preset
uuidV7:
type: string
format: uuid
x-uuidVersion: v7 # uuid version
httpsUrl:
type: string
format: uri
x-urlProtocol: '^https$' # url protocol regex
x-urlNormalize: true
preciseDatetime:
type: string
format: date-time
x-isoPrecision: 3 # iso datetime precision / offset / local
x-isoOffset: true| Extension | Maps to | Values |
| ---------------- | ------------------------------- | -------------------------------- |
| x-emailPattern | z.email({ pattern }) | html5 / browser / unicode |
| x-emailRegex | z.email({ pattern: /.../ }) | custom regex string |
| x-uuidVersion | z.uuid({ version }) | v1 / v4 / v6 / v7 / v8 |
| x-urlProtocol | z.url({ protocol: /.../ }) | regex string |
| x-urlHostname | z.url({ hostname: /.../ }) | regex string |
| x-urlNormalize | z.url({ normalize }) | true / false |
| x-isoPrecision | z.iso.datetime({ precision }) | fractional second digits |
| x-isoOffset | z.iso.datetime({ offset }) | true / false |
| x-isoLocal | z.iso.datetime({ local }) | true / false |
| x-macDelimiter | z.mac({ delimiter }) | : / - / . |
| x-jwtAlg | z.jwt({ alg }) | HS256 etc. |
| x-hashAlg | z.hash(alg, ...) | sha256 etc. |
| x-hashEnc | z.hash(alg, { enc }) | hex / base64 / base64url |
Branded Types
Use the x-brand vendor extension to generate Zod branded types, creating nominal types that are structurally identical but semantically distinct:
components:
schemas:
Cat:
type: object
properties:
name:
type: string
required:
- name
x-brand: Cat
Dog:
type: object
properties:
name:
type: string
required:
- name
x-brand: Dog// Generated output
const CatSchema = z.object({ name: z.string() }).brand<'Cat'>().openapi('Cat')
const DogSchema = z.object({ name: z.string() }).brand<'Dog'>().openapi('Dog')Projects Using Hono Takibi
- resend-local — A local emulator for the Resend email API.
Contributing
We welcome feedback and contributions to improve the tool!
If you find any issues with the generated code or have suggestions for improvements, please:
- Open an issue at GitHub Issues
- Submit a pull request with your improvements
License
Distributed under the MIT License. See LICENSE for more information.
