Migrating from ts-rest
Side-by-side comparison of ts-rest and Kizuna, for anyone weighing a ts-rest alternative, with notes on deliberate differences.
Kizuna is inspired by ts-rest and the ideas it pioneered. ts-rest showed that routes-first TypeScript APIs work, and a lot of people built real things with it, ourselves included. Kizuna borrows several of its core concepts and keeps the public API deliberately familiar, but takes a different approach under the hood. This page maps every changed API to its Kizuna equivalent.
Package mapping
| ts-rest | Kizuna | Notes |
|---|---|---|
@ts-rest/core | kizunajs + @kizunajs/fetch | @kizunajs/fetch writes the client into your app |
@ts-rest/express | @kizunajs/express | |
@ts-rest/next | @kizunajs/next | |
@ts-rest/open-api | @kizunajs/openapi |
The biggest difference
In ts-rest a route is a shape, and its implementation lives in a parallel tree you keep in step by hand. In Kizuna a route carries the handler that answers it:
Most of the mapping below follows from that: there is no router to build, and nothing to keep aligned.
Routes
initContract → new Kizuna() + k.routes
There is no initContract. Create your surface with new Kizuna() once, then declare groups with k.routes.
Config is written by kizuna generate from your kizuna.config.ts. It is what types every handler and checks every name you write. See new Kizuna().
Nested routes
The group you reach on k.routes, one you declared with k.groups, sets the OpenAPI tag for every route in it.
c.noBody() → z.void()
ts-rest uses c.noBody() to mark a route as having no request body, or a response as having no body. In Kizuna, use z.void() for the response and omit body entirely.
Assembling it
ts-rest passes its router straight to the client and the server. Kizuna assembles everything in one kizuna.config.ts at the root of your app, which is also where the CLI looks:
adapter is the one field with no ts-rest counterpart. Naming a framework there is what types every handler's req, c or request, so you never import your framework's types into a route file.
See defineConfig.
Client
Where ts-rest hands the contract to the client at runtime, Kizuna writes the client out ahead of time. Your config holds the handlers, so it never reaches a browser; name the file you want under clients and run kizuna generate:
See Clients.
Server
initServer → the route's own handler
There is no initServer and no router. The implementation sits on the route.
RouterImpl has no counterpart
ts-rest types a handler tree because the tree is where handlers live. In Kizuna the handler sits on the route, typed from the declaration above it, so there is nothing to annotate and no second tree to keep in step.
Express adapter
createExpressEndpoints → kizuna.api.mount
Next.js adapter
Route file setup
Authentication
ts-rest leaves authentication to your framework's middleware. Kizuna declares it: an identity says how the credential travels and what a passing guard provides, the guard sits on that identity, and a route's auth names the one it requires.
Handlers of a route with auth: 'user' then read auth.user.userId, typed, and the identity is emitted into your OpenAPI document as a security scheme. See Authentication.
Zod version
Kizuna targets Zod 4 only. ts-rest supports Zod 3 and 4.
If you are upgrading from ts-rest and Zod 3, migrate to Zod 4 alongside this change:
| Zod 3 / ts-rest | Zod 4 / Kizuna |
|---|---|
c.noBody() | z.void() or omit body entirely |
z.ZodTypeAny | z.ZodType |
ZodIssue (from 'zod') | z.core.$ZodIssue |
Error responses
Kizuna returns RFC 9457 Problem Details (application/problem+json) for every error response. Error responses must use ProblemDetailsSchema (or ProblemDetailsSchema.extend({...}) for extra fields), not an arbitrary shape. Validation errors are Problem Details with a detail string and an errors array, instead of ts-rest's { message, issues }. If you are porting an API whose clients expect the old shape, see Migrating an existing API for how to keep old clients working during the transition.
Method mismatch behaviour
ts-rest returns 404 for requests to a known path with an unsupported method. Kizuna returns 405 with an Allow header per RFC 9110 §15.5.6.