API Reference

Kizuna.requestContext

Provide request-scoped values to every handler, typed, such as analytics ids, loggers, and tenants.

Not everything that runs before a handler is auth. Analytics ids, request loggers, and tenant lookups are values every handler wants, gating nothing. Declare them with Kizuna.requestContext, register them on new Kizuna(), resolve each with the adapter's server.requestContext, and every handler receives them typed under requestContext, keyed by their name.

pnpm add @ts-kizuna/express
bun add @ts-kizuna/express
npm install @ts-kizuna/express
import { KizunaServer } from '@ts-kizuna/express';
pnpm add @ts-kizuna/fastify
bun add @ts-kizuna/fastify
npm install @ts-kizuna/fastify
import { KizunaServer } from '@ts-kizuna/fastify';
pnpm add @ts-kizuna/hono
bun add @ts-kizuna/hono
npm install @ts-kizuna/hono
import { KizunaServer } from '@ts-kizuna/hono';
pnpm add @ts-kizuna/next
bun add @ts-kizuna/next
npm install @ts-kizuna/next
import { KizunaServer } from '@ts-kizuna/next';

A declaration can bind to request headers. Those headers stay off the routes and out of the OpenAPI document, but the clients type them: the fetch client takes them on new KizunaClient(), and the generated Swift and Kotlin clients take them in their initializers, so a caller sets them once and the resolver reads them validated.

src/contract/request-contexts.ts
import { z } from 'zod';
import { Kizuna } from '@ts-kizuna/core';

export const analytics = Kizuna.requestContext({
    headers: z.object({
        'x-posthog-session-id': z.string().optional(),
    }),
    context: z.object({
        sessionId: z.string().nullable(),
    }),
});
src/contract/k.ts
import { analytics } from './request-contexts';

export const k = new Kizuna({
    requestContext: {
        analytics,
    },
});
src/server/request-context.ts
import { server } from './server';

export const captureAnalytics = server.requestContext('analytics', ({ headers }) => ({
    sessionId: headers['x-posthog-session-id'] ?? null,
}));
src/server/api.ts
export const api = server.api({
    router,
    requestContext: {
        analytics: captureAnalytics,
    },
});
listUsers: ({ query, requestContext }) => {
    track(requestContext.analytics.sessionId, 'listUsers');
    // ...
},

Sending values from a client

The fetch client takes the declared headers under requestContext and sends them with every request, required when a header is required and optional otherwise:

const apiClient = new KizunaClient(contract, {
    baseUrl: 'https://api.example.com',
    requestContext: {
        'x-posthog-session-id': sessionId,
    },
});

The generated Swift and Kotlin clients take a RequestContext in their initializer:

let client = APIClient(baseURL: url, requestContext: .init(xPosthogSessionId: sessionId))
val client = APIClient(baseUrl = baseUrl, requestContext = APIClient.RequestContext(xPosthogSessionId = sessionId))

Resolvers run on every route, public ones included, before the guards, and never deny a request. They receive the adapter's native request objects, the route's params, and the declared headers (validated). The return is checked against the context schema, and server.api requires a resolver for every declared key. Request context never appears in the OpenAPI document.

On this page