Fetch
A fully typed HTTP client for consuming Kizuna routes in the browser, on the server, or in React Native.
@kizunajs/fetch writes a typed client for your routes, a wrapper around the native fetch API. The client runs anywhere fetch does, React Native included.
The generated file carries named types, a table of methods and paths, and the code that sends the requests. It imports nothing, so the app that calls your API installs nothing. @kizunajs/fetch goes beside kizuna.config.ts:
What it writes
Every model keeps the title you gave Kizuna.model, and is declared once however many routes use it. Below the types come the route table, createClient, and the code that sends requests.
Create the client
The generated file exports createClient. Wrap it once, where the rest of your app imports it from:
The client mirrors your route groups, one function per route.
Call a route
params is required when the path contains :param placeholders, and rejected when it does not.
When the route declares a pathParams schema, params is typed by that schema instead of the path template. A branded ID reaches the caller:
ClientConfig
| Option | Description |
|---|---|
baseUrl | Base URL prepended to every route path |
baseHeaders | Headers merged into every request |
credentials | Passed as credentials to every fetch call |
fetch | Custom fetch implementation (e.g. for testing or a polyfill) |
onRequest | Callback before each request, receiving the OutgoingRequest it is about to send |
Setting a header on the headers of an OutgoingRequest sets it on the request that goes out, which is how a credential that lives in storage is read per request rather than fixed at construction. See React Native.
Request context
When your config declares request context headers, createClient takes them too, typed by what the API asked for, and sends them on every request. This is a different thing from the OutgoingRequest above: these are the headers your API declared with k.requestContext, and the generated file exports their type as RequestContext:
An API that declares none takes ClientConfig alone.
Per-request headers
If a route declares a headers schema, the client requires those headers:
Routes without a headers schema accept an optional headers?: Record<string, string> for arbitrary request headers.
Authentication
When your routes name an identity with identities, send the credential in baseHeaders. The server's guards read it, and answer 401/403 when it's missing or insufficient:
Per-request fetch options
Pass any RequestInit option via fetchOptions:
Response type
Each call returns a discriminated union over the route's defined status codes:
Every route's types are in the namespace, so API.UsersListUsers.Query is what listUsers takes as query, and API.UsersListUsers.Result is what it resolves to. Wrapping the client builds on them.
Streams
On a status the route declares with stream, body is an async iterable in place of a value. Read it with for await, and each message narrows on event to its own data:
The other statuses arrive parsed as they do today. Aborting the signal in fetchOptions closes the connection.
Validation errors
A route with a body or query schema can answer 400 when the request fails validation. It is in the response type already, without you declaring it.
A route with neither takes no 400 in its response type.
isValidationError
If your routes also declares a 400 response on the same route, the body becomes a union of your type and ValidationError. Use the isValidationError type guard to distinguish them:
The generated file exports it:
Refusals
A route whose auth names an identity carries 401 and 403 in its response type, as Problem Details. A public route carries neither. A route declaring its own 403 keeps that body alongside, the way a declared 400 sits beside the validation error.
React Native
The client works in Expo and in bare React Native, importing the same generated client as your website.
Reading a token from storage
baseHeaders is fixed when the client is constructed, so a credential that lives in storage goes in onRequest. It runs before every request, and the Headers it receives are the ones sent:
Uploading a file
Expo SDK 54 and later ships a File class, and expo/fetch is the implementation that puts one inside a multipart body. Hand the client Expo's:
Then build the FormData yourself, since the client forwards a FormData body untouched:
The cast is there because the route types the field as the File the server receives once the multipart request lands.
Bare React Native has no File it can build from a path. Append a { uri, name, type } descriptor in its place and leave fetch out of the client config, since that form is the one the global fetch understands.
Named types
The generated file gives every model a name, the thing the Swift and Kotlin clients have always had:
users.getUser becomes UsersGetUser in every generated client, so the three read as one API.
The namespace is called API until the config says otherwise, and namespace names it whatever the app calls its API. Every route type is reached through that name, so MyAPI.UsersGetUser.Params.
Branded IDs
A branded ID becomes a named type, API.UserId, and a toUserId function that makes one. The parameter's type shows it, and passing a plain string does not compile.
An ID from a response is already an API.UserId, so it passes into the next call as it is:
A string from outside the API, like a URL, is wrapped once:
It is still a string, so it renders and compares like one.
Documentation in the client
Each method carries what the route declared, so an editor shows it at the call site: the route's summary and description, @deprecated with its migration message, and an @example of the call with the arguments it cannot be called without.
Fields carry theirs too. A schema's description becomes the field's doc comment, and anything it declares under example or examples in Zod metadata becomes an @example:
The same metadata is what the OpenAPI document publishes as its examples, so the two never drift.
What ships
The generated file imports nothing. It carries no schema library, no handlers and no server code. The table it carries is method, path, content type, and each status the route answers with, marking the ones that stream:
That is all the code below it reads. The types above it are erased at build time.
kizuna generate writes the table and the code that reads it into one file, so the two always come from the same release. Upgrading kizuna changes the client only when you regenerate it.
Keeping it current
The file can fall behind your routes, and it fails loudly when it does: add a route and the client has no method for it, which your compiler catches. Regenerate it as part of your build and have CI fail on a dirty tree afterwards.