Create an Adapter
Build and ship your own Kizuna adapter for any HTTP runtime using createAdapter from kizunajs/adapter.
The adapter API is new and still settling. createAdapter and the AdapterResult shape may change while v2 is in beta, so pin your version if you depend on them.
kizunajs/adapter exposes createAdapter so third parties can build and ship their own adapters independently. Kizuna ships Express, Fastify, Hono, and Next.js.
Whether an adapter goes first-party comes down to adoption, not age. If a framework picks up real usage, we will very likely add it. What we will not take on is a framework a handful of people use, since we maintain everything we merge. The adapter API is public, so you can build and publish your own today.
Overview
An adapter translates between a native HTTP runtime (Bun, Cloudflare Workers, Deno, etc.) and Kizuna's handler pipeline. You provide three things:
buildHandlerContextbuilds the context object your handlers receive alongside validated inputsrespondtranslates anAdapterResultto a native responseonError(optional) intercepts unhandled handler errors
createAdapter
import { createAdapter, renderJsonResult, parseFetchBody, headersToObject, type AdapterRequest } from 'kizunajs/adapter';Minimal example (fetch-based runtime)
import {
createAdapter,
renderJsonResult,
parseFetchBody,
headersToObject,
type AdapterRequest,
type Routes,
type Router,
} from 'kizunajs/adapter';
export interface HandlerContext {
request: Request;
}
const adapter = createAdapter<Request, Response, HandlerContext>({
buildHandlerContext: (adapterRequest) => ({
request: adapterRequest.request,
}),
respond: (result, { request }) => {
if (result.kind === 'raw-response') return result.response as Response;
const rendered = renderJsonResult(result);
if (rendered.stream) {
return new Response(rendered.stream({ signal: request.signal }), {
status: rendered.status,
headers: rendered.headers,
});
}
return new Response(rendered.body === null || rendered.body === undefined ? null : JSON.stringify(rendered.body), {
status: rendered.status,
headers: rendered.headers,
});
},
});
export const handleRequest = <T extends Routes>(
request: Request,
routes: T,
router: Router<T, HandlerContext>,
options?: { basePath?: string }
): Promise<Response> => {
const url = new URL(request.url);
const adapterRequest: AdapterRequest<Request> = {
request,
method: request.method,
resolution: {
kind: 'core-match',
path: url.pathname,
},
query: Object.fromEntries(url.searchParams),
headers: headersToObject(request.headers),
readBody: (route) => parseFetchBody(request, route),
};
return adapter.handle({
routes,
router,
request: adapterRequest,
responseContext: {},
basePath: options?.basePath,
});
};AdapterDefinition
interface AdapterDefinition<NativeRequest, NativeResponse, HandlerContext, ResponseContext = Record<string, never>> {
buildHandlerContext: (request: AdapterRequest<NativeRequest>, context: ResponseContext) => HandlerContext | Promise<HandlerContext>;
respond: (result: AdapterResult, context: ResponseContext) => NativeResponse | Promise<NativeResponse>;
onError?: (error: unknown, request: AdapterRequest<NativeRequest>) => AdapterResult | void | Promise<AdapterResult | void>;
matcher?: RouteMatcher;
}| Field | Description |
|---|---|
buildHandlerContext | Returns the context object each handler receives alongside params, query, body, headers. |
respond | Translates an AdapterResult to a native response. Always handle kind: 'raw-response' first, as it carries a pre-built native response from onError. |
onError | Called on unhandled handler errors. Return an AdapterResult to override the default 500, or return void to let it pass through. |
matcher | Custom route matcher. Defaults to Kizuna's built-in path matcher. |
AdapterRequest
interface AdapterRequest<NativeRequest> {
request: NativeRequest;
method: string;
resolution:
| { kind: 'core-match'; path: string } // Next-style: core matches the path
| { kind: 'pre-resolved'; routeKey: string; route: RouteDefinition; params: Record<string, string> }; // Express-style: adapter already routed
query: unknown;
headers: unknown;
readBody: (route: RouteDefinition) => Promise<unknown> | unknown;
}Use kind: 'core-match' for catch-all routing (one handler handles all paths). Use kind: 'pre-resolved' for per-route registration (Express-style, where the framework has already matched the route).
AdapterResult
respond receives one of these from the pipeline:
kind | When |
|---|---|
success | Handler returned a valid response |
not-found | No route matched the path |
method-not-allowed | Path matched but method is not in the routes |
validation-failed | params, query, headers, or body failed schema validation |
handler-error | Handler threw an unhandled error |
raw-response | onError returned an override, so cast result.response back to NativeResponse |
Helpers
renderJsonResult
Translates any AdapterResult (except raw-response) to { status, headers, body } using Kizuna's defaults, such as 405 with an Allow header and validation errors as Problem Details (detail plus an errors array).
const rendered = renderJsonResult(result);
// rendered.status, rendered.headers, rendered.bodyPass the request's method as the fourth argument when your adapter writes the wire bytes itself: a HEAD request then keeps GET's status and headers, gains a Content-Length, and loses the content (RFC 9110 §9.3.2). Leave it unset when the framework discards HEAD content itself, as Express, Fastify, and Hono do.
const rendered = renderJsonResult(result, formatError, request, request.method);A success result on a status declared with stream renders with body undefined and a stream function in its place. Check stream before body, hand it the signal that fires when the client goes away, and write the ReadableStream it returns. encodeStreamBody(result, { signal }) does the same for an adapter that renders results itself.
parseFetchBody
Content-type-aware body parsing for fetch-based runtimes (Request / Response API). Handles application/json, multipart/form-data, and application/x-www-form-urlencoded.
readBody: (route) => parseFetchBody(request, route),headersToObject
Converts a Headers instance to Record<string, string>.
headers: headersToObject(request.headers),