Build fully typed REST APIs in TypeScript

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

Get started
npm install kizunajs@beta
Beta

Battle-tested in production. v2 is in beta, so pin your version and read the release notes between releases.Battle-tested in prod.

Read the FAQ

One config.
Everything reads from it.

Write it in TypeScript with Zod. The clients, the OpenAPI spec, and request validation follow, typed end to end.

RoutesAuthJobsPlugins
kizuna.config.ts
api-client.ts
const apiClient = createClient({  baseUrl: 'http://localhost:3000',});const res = await apiClient.users.getUser({  params: {    id: '1',  },});if (res.status === 200) {  res.body; // User, fully typed} else {  throw new Error(res.body.detail);}
Clients

Typed fetch and TanStack Query, plus generated Swift and Kotlin.

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'
OpenAPI

A complete spec generated from your routes, no annotations needed.

localhost:3000
POST /users HTTP/1.1{ "name": "Ada", "email": "nope" }HTTP/1.1 400 Bad Requestcontent-type: application/problem+json{  "title": "Bad Request",  "detail": "Invalid request body",  "errors": [    {      "code": "invalid_format",      "path": ["email"],      "message": "Invalid email address"    }  ]}
Validation

Bad requests answered as Problem Details before your handler runs.

Your API, ready for AI agents

Mark a route as a tool and any AI assistant can call it, behind the same types and guards as the rest of your API.

ClaudeChatGPTCopilot
Your APIMCP endpoint

users

GETusers_list_users

GETusers_get_user

POSTusers_create_user

DELETEusers_delete_user

MCP endpoint

Install the MCP plugin, and every route that declares a tool is one AI assistants can discover and call.

Claude
>Clean up the test users from yesterday's demo
●
users_list_users(name: "Test")read-only
⎿200 OK [{ "id": "51", "name": "Test User" }]
users_delete_user(id: "51")destructive
Do you want to proceed?
❯ 1. Yes
2. Yes, and don't ask again this session
3. No
●
users_delete_user(id: "51")
⎿204 No Content
●
Found one test user and deleted it.
Annotated tools

Each tool carries the annotations of its HTTP method, so an assistant reads freely and asks before it deletes.

Your handler, fully typed

Params, body, auth, jobs and the rest arrive validated, so a wrong name fails to compile instead of failing in production.

getReport: k    .route({        method: 'GET',        path: '/reports/:year/:month',        responses: {            200: ReportSchema,        },    })    .handler(async ({ params }) => {        const report = await db.reports.findFirst({            where: {                year: params.
params

Typed from the path string itself. Rename a param and every handler that reads it fails to compile.

Ship your API as an SDK

Publish what Kizuna generates, and your customers get the typed SDK you would otherwise write and maintain by hand.

src/app.ts
import { AcmeClient } from '@acme/sdk';const acme = AcmeClient({    baseUrl: 'https://api.acme.com',});const user = await acme.users.getUser({    params: {        id: '42',    },});
npm

A TypeScript package for browsers, Node and React Native.

UserService.swift
import AcmeSDKlet acme = AcmeClient(    baseURL: URL(string: "https://api.acme.com")!)let user = try await acme.users.getUser(    .params(        id: "42"    ))
Swift Package Manager

A Swift package with no dependencies, ready for iOS and macOS apps.

UserRepository.kt
import com.acme.sdk.AcmeClientval acme = AcmeClient(    baseUrl = "https://api.acme.com")val user = acme.users.getUser {    params(        id = "42"    )}
Maven

A Kotlin library for Android and JVM apps, built on OkHttp and kotlinx.serialization.

Ship without breaking apps

Kizuna compares every change against main and fails the pull request on anything that would break a consumer of your API.

Deprecate it

Serve email and email_address together until the sunset date you set. Nothing on the old name breaks.

Break it on purpose

Label the pull request breaking changes. CI stays red until someone signs off on it.

RFC-correct out of the box

Every status code, error body and header follows the RFC that defines it, without you reading a single spec.

  • RFC9457Problem Details errors
  • RFC8594Sunset dates
  • RFC9745Deprecation headers
  • RFC9111Cache-Control and Vary
  • RFC9110ETag and 304 responses
  • WHATWGSSETyped event streams
  • OpenAPI3.1Generated from your routes
  • OAuth2.1Resource server
  • RFC9728Discovery document
  • RFC6750Scope challenges
  • ProtocolMCPTools a model can call
  • Specs+7And many more
Standards

Every specification Kizuna follows, what each one covers, and what it leaves out.

Ready to build?

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