Config
Assemble your API in kizuna.config.ts, mount it, and say where its generated clients go.
kizuna.config.ts sits at the root of your app, beside package.json. It assembles everything the API is made of, hands back the api you mount, and says where each generated client is written. The tooling looks for it by name.
The file
It default-exports two things: api, which you mount and which every generator reads, and clients, which the CLI writes.
adapter decides what every handler receives beside its validated inputs. Express here puts req and res in scope for all of them, typed.
See defineConfig for every field it takes.
What each key brings
| Key | What it brings |
|---|---|
auth | The identities a route's auth can require. Each carries its own guard, so this is all the config needs. An identity used without a guard refuses to assemble. See Authentication. |
requestContext | Values every handler receives, resolved once per request. Resolvers run on every route, before the guards, and never deny. See Request Context. |
jobs | Scheduled work. api.mount serves the two job endpoints unless jobRunner.mode is 'in-process', and every handler receives a typed jobs runner so a route can run one in process. |
plugins | Routes, jobs and helpers installed beside your own, as a list. See below, and Create a Plugin to write one. |
validation | The code values k.issue may emit beyond Zod's own. |
groups | The groups routes are filed under, each an OpenAPI tag. See k.groups. |
Using what a plugin offers
A plugin can hand your handlers functions to call, under plugins:
Destructure it for the short form:
Job handlers receive plugins the same way.
Renaming one
Each plugin picks the slug handlers reach it under. When two want the same one, defineConfig throws and names both, and you rename one:
Middleware
Rate limiting, multipart parsing, and the rest go in your framework's own middleware (app.use, Fastify hooks). Authentication belongs in guards, request-scoped values in requestContext.
Clients
Each client comes from the package that generates it, so its options are typed there and a generator you have not installed cannot be configured by accident. The list is not closed: any package that exports a client built with defineClient drops into clients the same way.
| Client | Import from | Writes |
|---|---|---|
fetchClient | @kizunajs/fetch | A typed TypeScript client |
swiftClient | @kizunajs/swift | A native Swift client |
kotlinClient | @kizunajs/kotlin | A native Kotlin client |
Every route reaches every client. A route no app calls, like a health check or a webhook, is hidden and stays out of them.
Files that aren't clients, like the OpenAPI document, come from plugins. openApiPlugin writes openapi.yaml when you give it an output.
The generated client holds the route shapes and the schemas, and is what a browser imports. An output is resolved against the config's own directory, so in a repository holding both, ../ios/MyApp/Generated/APIClient.swift writes into the app beside it.
Client options
| Option | Applies to | Description |
|---|---|---|
output | all | Path the generated file is written to |
namespace | Fetch, Swift, Kotlin | Name of the generated namespace, API by default |
package | Kotlin | Package the generated file declares |
camelCaseProperties | Swift, Kotlin | Convert wire field names to camelCase, mapping the wire name back |
unknownEnumCase | Swift, Kotlin | Give every enum an unknown case, so an unrecognised value still decodes |
Two clients of one language
Add two entries.
More than one API
One config is one API. A second config is a second API, with its own routes, its own identities and its own generated clients.
When to split
Split when the two have different callers. A public API versioned for outside integrators and an internal admin API answer to different people, change on different schedules, and want different clients, so one OpenAPI document describing both serves neither.
Split when they are deployed apart. A config has one adapter, so an API on Express and an API on Next.js are already two configs.
Split when the surfaces should not mix. Everything in one config shares a namespace: every route reaches every plugin's exports, one guardSchema shapes every refusal, and every route lands in the same generated client. Two configs keep those apart.
Stay with one config when the routes differ only by prefix. A /admin group under the same identities is a route group, not a second API, and splitting it costs you the shared types.
Two configs
Give each one a file, and each its own clients:
Each config writes its own Config, so each gets its own k:
A route declared against the admin k sees the admin identities and nothing else, so a public identity on an admin route is a type error rather than a runtime surprise.
Running them
kizuna generate reads one config, so point it at each in turn:
Each config declares its own clients, so the Swift and Kotlin clients for the admin API come out of kizuna.admin.config.ts along with its types.
Sharing between them
What both APIs need lives in a module they both import: schemas, models, an identity, or a whole route group. Kizuna.model keeps a shared schema's name, so User is one named type in both generated clients rather than two shapes that happen to match.
Serving both from one process is ordinary mounting. On Express, mount takes anything with a use, so a router mounted under a path keeps the admin API off the public one:
On Next.js each API is its own catch-all route file, and mount({ basePath }) is what tells it which prefix it answers on.
kizuna generate
It writes four things from kizuna.config.ts: the Config in kizuna.types.ts, every client under clients, the files plugins generate, like openApiPlugin's openapi.yaml, and the .kizuna/snapshot.yaml that kizuna diff compares. A file that already matches is left alone, so watchers and build tools see no change.
| Option | Default | Description |
|---|---|---|
--check | off | Report what would be rewritten and write nothing. See below |
--config | kizuna.config.ts | The config to read |
--types | the config's own | Where the Config is written, overriding typescript.outputFile |
Where the types land is usually the config's business rather than the command's:
Wire it into your scripts
Two scripts cover everywhere it needs to run:
Run kizuna:generate after changing a route, an identity, or the config. Run kizuna:check in CI, where it fails on anything nobody regenerated.
Whoever changes your routes needs to know about that first script, and a coding agent working in the repository reads the same files your team does. Say it where they look:
kizuna.types.ts
The Config interface is read from your config's syntax, not by running it. That is what lets k be typed by the config that serves your routes without the routes waiting on a config that loads.
It carries adapter, groups, auth, requestContext, validation, jobs and plugins. Your routes stay out: they are read at runtime, and typing against them would make the config a prerequisite for declaring the routes it serves.
Point new Kizuna<Config>() at it once and every name you write is checked against it:
kizuna generate --check
The same work without writing. It names whatever has fallen behind and exits 1, so a pipeline fails on a file nobody regenerated:
Commit what kizuna generate writes. The check compares the files on disk against the config, so a generated client that is gitignored reads as missing on every clean checkout and the pipeline fails on it.
Run it in CI, where the CLI page has the workflow. kizuna diff reads the snapshot this writes, so the two belong in the same pipeline: the check proves the snapshot matches today's config, and the diff compares it against the base.
writeClients and checkClients from @kizunajs/cli are the same two steps as functions, for a script that does more than the command does.
Reference
defineConfig- Routes for what you put under
routes - Fetch client, Swift, Kotlin