Build fully typed REST APIs with TypeScript

Write one contract. Get a fully typed server, an OpenAPI spec, Swift and Kotlin clients, and more.

Beta

Battle-tested in production. The syntax may still change before v2, so pin your version.

Read the FAQ

OpenAPI generation

From the same contract, no annotations needed.

Native client generation

Typed API clients for Swift (iOS/macOS) and Kotlin (Android/JVM).

MCP server generation

Expose your API as MCP tools so AI assistants can call your endpoints.

Contract-first

Define request and response schemas once, share them between server and client.

Typed auth

Identities and per-route auth declared on the contract.

Adapters

Mount your API on Express, Fastify, Hono, or Next.js.

Plugins

Extend your API with features built on the contract you already wrote, fully typed in your handlers.

Scheduled jobs

Declare cron work next to its handler and tick it from any platform scheduler, or run it in process.

RPC-like client

Call your API like a function, get fully typed responses back.

HTTP/REST

Follows HTTP and REST standards. RFC 9110 semantics, RFC 9457 Problem Details.

Built-in coercion

Query, path, and header params are coerced to their declared types. No manual parsing, no z.coerce.

Deprecation support

Mark endpoints and fields with a JSDoc @deprecated tag. IDEs, OpenAPI, Swift, and Kotlin all pick it up.

The idea

One contract is the source of truth. Your server, your clients, and every generated artifact read from it.

contract.ts
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
Validated inputs, type-checked responses
router.ts
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,      };    },  },});
OpenAPI
Generated from the contract
openapi.yaml
/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'
REST
Every route is a real REST endpoint
localhost:3000/users/1
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"}
TS client
RPC-like, call routes like functions
api-client.ts
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);}
Swift client
Native generated client for iOS & macOS
UserService.swift
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)}
Kotlin client
Native generated client for Android & JVM
APIClient.kt
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}
MCP server
Routes become tools for AI agents
k.ts
plugins: {  mcp: mcpPlugin()}// each route → a typed MCP tool:// GET → read-only · DELETE → destructive// PUT → idempotent
Deprecation
Mark once, it propagates everywhere
routes.ts
/** @deprecated */deleteUser: { ... }// → editor strikethrough// → OpenAPI deprecated: true// → Swift @available · Kotlin @Deprecated

Inside a handler

Whatever the contract declares, the handler gets it validated and typed.

users.router.ts
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.

Runs anywhere

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.

Start here

Ready to build?

8 minutes from an empty file to a typed client calling a real endpoint.