Adapters

Express

Mount a Kizuna API on an Express 5 application.

@kizunajs/express serves a Kizuna API from an Express 5 application. It handles routing, request validation, body parsing, and error formatting, all driven by what you declared.

Requires Express ≥ 5.

pnpm add @kizunajs/express@beta express
bun add @kizunajs/express@beta express
npm install @kizunajs/express@beta express

The adapter

expressAdapter() is what you name in kizuna.config.ts. Naming it is what gives every handler on this API Express's own req and res, typed.

Declare a route

src/routes/users.ts
import { k } from '../k';

export const users = k.routes('users', {
    listUsers: k
        .route({
            method: 'GET',
            path: '/users',
            query: ListUsersQuerySchema,
            responses: {
                200: UserListSchema,
            },
        })
        .handler(async ({ query }) => ({
            status: 200,
            body: {
                users: await db.users.findMany({
                    skip: (query.page - 1) * query.limit,
                    take: query.limit,
                }),
                total: await db.users.count(),
            },
        })),
    getUser: k
        .route({
            method: 'GET',
            path: '/users/:id',
            responses: {
                200: UserSchema,
                404: ProblemDetailsSchema,
            },
        })
        .handler(async ({ params }) => {
            const user = await db.users.findById(params.id);
            if (!user) {
                return {
                    status: 404,
                    body: {
                        detail: 'Not found',
                    },
                };
            }
            return {
                status: 200,
                body: user,
            };
        }),
});

Name Express in your config

kizuna.config.ts
import { defineConfig } from 'kizunajs';
import { expressAdapter } from '@kizunajs/express';
import { users } from './src/routes/users';

export default defineConfig({
    adapter: expressAdapter(),
    routes: {
        users,
    },
});

Mount it

src/index.ts
import express from 'express';
import kizuna from '../kizuna.config';

const app = express();
app.use(express.json());

kizuna.api.mount(app);

app.listen(3000);

Handler context

Each handler receives { params, query, body, headers, req, res }. The req and res are the native Express objects, useful when you need to read cookies, set custom headers, or stream a response.

src/routes/users.ts
.handler(async ({ params, req, res }) => {
    console.log(req.ip);
    res.setHeader('x-custom', 'value');
    return {
        status: 200,
        body: await db.users.findById(params.id),
    };
});

Guards

A guard sits on the identity it authenticates, and receives the credential extracted and typed, alongside the native req/res. See the Authentication guide.

src/identities.ts
import { k } from './k';

export const user = k.identity
    .bearer({
        context: z.object({
            userId: z.string(),
        }),
    })
    .guard(async ({ bearer, deny, req }) => {
        const session = bearer ? await verifySession(bearer.token) : undefined;
        if (!session) {
            return deny({
                status: 401,
                body: {
                    detail: 'Unauthorized',
                },
            });
        }
        return {
            userId: session.userId,
        };
    });

Middleware

For request-scoped values handlers need, use request context. Rate limiting, multipart parsing and the rest go in Express's own app.use. Authentication belongs in guards.

Options

src/index.ts
kizuna.api.mount(app, {
    responseValidation: false,
});
OptionDefaultDescription
responseValidationfalseValidate handler return values against response schemas, surfacing as 500 on mismatch. Enable in development.
formatErrornoneReshape error (>= 400) response bytes for migrating clients. Most don't need it (use Problem Details extension members). See Migrating an existing API.

Type-safe request properties

Middleware that sets custom properties on req, such as a request id, needs declaration merging to extend the Express Request interface. Authentication data does not: guards hand handlers typed context directly.

express.d.ts
declare global {
    namespace Express {
        interface Request {
            requestId: string;
        }
    }
}

Both middleware and handlers are then typed:

src/routes/users.ts
.handler(async ({ params, req }) => {
    console.log(req.requestId);
    // ...
});

Method mismatches

If a path is matched but the method is not declared, the adapter returns 405 with an Allow header listing the supported methods. This follows RFC 9110 §15.5.6.

Multipart / file uploads

For routes with contentType: 'multipart/form-data', add a multipart middleware such as multer or busboy before the Kizuna handler. The adapter reads req.body as-is.

src/index.ts
import multer from 'multer';

const upload = multer({
    storage: multer.memoryStorage(),
});

app.post('/avatar', upload.single('file'), (req, res, next) => {
    next();
});

kizuna.api.mount(app);

Streams

A stream response is piped to res after flushHeaders(), so the client sees the headers before the first message. The generator's signal fires when res closes before the stream has finished.

Reference

On this page