OpenAPI generation
From the same contract, no annotations needed.
Write one contract. Get a fully typed server, an OpenAPI spec, Swift and Kotlin clients, and more.
Battle-tested in production. The syntax may still change before v2, so pin your version.
Read the FAQFrom the same contract, no annotations needed.
Typed API clients for Swift (iOS/macOS) and Kotlin (Android/JVM).
Expose your API as MCP tools so AI assistants can call your endpoints.
Define request and response schemas once, share them between server and client.
Identities and per-route auth declared on the contract.
Mount your API on Express, Fastify, Hono, or Next.js.
Extend your API with features built on the contract you already wrote, fully typed in your handlers.
Declare cron work next to its handler and tick it from any platform scheduler, or run it in process.
Call your API like a function, get fully typed responses back.
Follows HTTP and REST standards. RFC 9110 semantics, RFC 9457 Problem Details.
Query, path, and header params are coerced to their declared types. No manual parsing, no z.coerce.
Mark endpoints and fields with a JSDoc @deprecated tag. IDEs, OpenAPI, Swift, and Kotlin all pick it up.
One contract is the source of truth. Your server, your clients, and every generated artifact read from it.
export const k = new Kizuna();const UserSchema = Kizuna.model({ // shows up as a named User in OpenAPI, Swift, and Kotlin title: 'User', schema: z.object({ id: z.string(), name: z.string(), }),});const users = k.routes({ getUser: { method: 'GET', path: '/users/:id', responses: { 200: UserSchema, 404: ProblemDetailsSchema, // or ProblemDetailsSchema.extend({ ... }) to add extra fields }, },});export const contract = k.contract({ routes: { users, },});server.router({ users: { getUser: async ({ params, throwError }) => { const user = await db.users.findById(params.id); if (!user) throwError({ status: 404, body: { detail: 'Not found', }, }); return { status: 200, body: user, }; }, },});/users/{id}: get: operationId: getUser parameters: - name: id in: path required: true schema: type: string responses: '200': content: application/json: schema: $ref: '#/components/schemas/User' '404': content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails'HTTP/1.1 200 OKContent-Type: application/json{ "id": "1", "name": "Ada"}HTTP/1.1 404 Not FoundContent-Type: application/problem+json{ "type": "about:blank", "status": 404, "detail": "Not found"}const client = new KizunaClient(contract, { baseUrl: 'http://localhost:3000',});const res = await client.users.getUser({ params: { id: '1', },});if (res.status === 200) { res.body; // User, fully typed} else { throw new Error(res.body.detail);}let client = APIClient( baseURL: URL(string: "http://localhost:3000")!)do { let res = try await client.users.getUser( .params( id: "1" ) ) res.body // User, Codable} catch { error // typed failure (e.g. .notFound)}val client = APIClient( baseUrl = "http://localhost:3000")try { val res = client.users.getUser { params( id = "1" ) } res.body // User, @Serializable} catch (error: APIClient.UsersGetUser.Failure.NotFound) { error.body.detail // typed failure}plugins: { mcp: mcpPlugin()}// each route → a typed MCP tool:// GET → read-only · DELETE → destructive// PUT → idempotent/** @deprecated */deleteUser: { ... }// → editor strikethrough// → OpenAPI deprecated: true// → Swift @available · Kotlin @DeprecatedWhatever the contract declares, the handler gets it validated and typed.
export const users: Router<typeof contract.routes.users> = { getUser: async ({ params }) => { const user = await db.user.findFirstOrThrow({ where: { id: params.userId, workspaceId: params.workspaceId, }, }); return { status: 200, body: user, }; },};params: { workspaceId: string; userId: string;}Autocomplete knows every param in the path. Change the path and your editor points at every handler to update.
The same contract and router move between adapters, and the framework underneath stays available to you.
Express
req, res
Fastify
request, reply
Hono
c, c.env
Next.js
request
Every handler gets params, query, body, and headers validated the same way, and each adapter hands you its own primitives on top. Through Hono the same API runs on Cloudflare Workers, Deno, and Bun.
Ready to build?
8 minutes from an empty file to a typed client calling a real endpoint.