Adapters

Fastify

Mount a Kizuna API on a Fastify application.

@kizunajs/fastify serves a Kizuna API from a Fastify application.

Requires Fastify >= 5.

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

The adapter

fastifyAdapter() is what you name in kizuna.config.ts. Naming it is what gives every handler on this API Fastify's own request and reply, 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 Fastify in your config

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

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

Mount it

Fastify registers plugins asynchronously, so mount returns a promise:

src/index.ts
import Fastify from 'fastify';
import kizuna from '../kizuna.config';

const app = Fastify();

await kizuna.api.mount(app);

app.listen({
    port: 3000,
});

Handler context

Each handler receives { params, query, body, headers, request, reply }. The request is Fastify's FastifyRequest and reply is FastifyReply, useful when you need raw headers, cookies, or anything else Fastify exposes.

src/routes/users.ts
.handler(async ({ params, request }) => {
    request.log.info({ id: params.id }, 'loading user');
    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 request/reply. 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 }) => {
        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. For other middleware, such as logging and rate limiting, use Fastify's own hooks (app.addHook('preHandler', ...)). Authentication belongs in guards.

Options

src/index.ts
await kizuna.api.mount(app, {
    responseValidation: false,
});
OptionDefaultDescription
responseValidationfalseValidate handler return values against response schemas. 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.

The same options go on fastifyKizuna when you register the plugin yourself:

src/index.ts
import { fastifyKizuna } from '@kizunajs/fastify';
import kizuna from '../kizuna.config';

await app.register(fastifyKizuna, {
    api: kizuna.api,
    responseValidation: false,
});

Type-safe request properties

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

fastify.d.ts
declare module 'fastify' {
    interface FastifyRequest {
        requestId: string;
    }
}

Both middleware and handlers are then typed:

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

The adapter also sets request.kizunaRoute on every matched request, holding the route that matched.

Streams

A stream response hijacks the reply and is piped to reply.raw after its headers are flushed. The generator's signal fires when the raw response closes before the stream has finished.

Reference

On this page