Adapters

Fastify

Mount a ts-kizuna API on a Fastify application.

@ts-kizuna/fastify connects a ts-kizuna API to a Fastify application.

Requires Fastify >= 5.

pnpm add @ts-kizuna/fastify fastify
bun add @ts-kizuna/fastify fastify
npm install @ts-kizuna/fastify fastify

Setup

The standard setup:

  • new KizunaServer() binds the contract to a server handle
  • server.router binds typed implementations to the contract
  • server.api combines the router and guards into an API object
  • fastifyKizuna is the Fastify plugin that mounts the API
src/server/server.ts
import { KizunaServer } from '@ts-kizuna/fastify';
import { contract } from '@shared/contract';

export const server = new KizunaServer(contract);
src/server/router/index.ts
import { server } from './server';

export const router = server.router({
    users: {
        listUsers: async ({ query }) => {
            const found = await db.users.findMany({
                skip: (query.page - 1) * query.limit,
                take: query.limit,
            });
            return {
                status: 200,
                body: {
                    users: found,
                    total: await db.users.count(),
                },
            };
        },
        createUser: async ({ body }) => {
            const user = await db.users.create(body);
            return {
                status: 201,
                body: user,
            };
        },
        getUser: async ({ params }) => {
            const user = await db.users.findById(params.id);
            if (!user) {
                return {
                    status: 404,
                    body: {
                        detail: 'Not found',
                    },
                };
            }
            return {
                status: 200,
                body: user,
            };
        },
    },
});
src/server/api.ts
import { server } from './server';
import { router } from './router';

export const api = server.api({
    router,
});
src/index.ts
import Fastify from 'fastify';
import { api } from './server/api';

const app = Fastify();
await api.mount(app);

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

Handler context

Each handler receives { params, query, body, headers, request, reply }. The request is the Fastify FastifyRequest and reply is FastifyReply, useful when you need access to raw headers, cookies, or other Fastify-specific features.

getUser: async ({ params, request }) => {
    const token = request.headers.authorization;
    const user = await db.users.findById(params.id);
    return {
        status: 200,
        body: user,
    };
},

Guards

One guard per identity. The credential arrives extracted and typed, alongside the native request/reply. See the Auth guide.

src/server/guards.ts
import { server } from './server';

export const requireUser = server.guard('user', async ({ bearer, deny }) => {
    const session = bearer ? await verifySession(bearer.token) : undefined;
    if (!session) {
        return deny(401, 'Unauthorized');
    }
    return {
        userId: session.userId,
    };
});

Middleware

For request-scoped values handlers need, use Kizuna.requestContext. For other middleware, such as logging and rate limiting, use Fastify's own hooks (app.addHook('preHandler', ...)). Authentication belongs in guards.

Options

Pass options alongside api when registering the plugin:

app.register(fastifyKizuna, {
    api,
    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.

Type-safe request properties

Auth data doesn't need request mutation, because guards return typed context that handlers receive directly. But if non-auth middleware sets custom properties on request, such as a request id, use Fastify's declaration merging to extend the FastifyRequest interface:

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

With the augmentation in place, both middleware and handlers are fully typed:

getUser: async ({ params, request }) => {
    console.log(request.requestId);
    // ...
},

The adapter also sets request.kizunaRoute on every matched request, containing the matched RouteDefinition. This is available without any extra type setup.

RouteHandler and Router types

Annotate handlers individually or as a whole tree:

import type { RouteHandler, Router } from '@ts-kizuna/fastify';
import { contract } from '@shared/contract';

// Single route
const getUser: RouteHandler<typeof contract.routes.users.getUser> = async ({ params }) => {
    const user = await db.users.findById(params.id);
    if (!user) {
        return {
            status: 404,
            body: {
                detail: 'Not found',
            },
        };
    }
    return {
        status: 200,
        body: user,
    };
};

// Whole handler tree
const router: Router<typeof contract> = { ... };

RouteHandler carries the route's auth and request context, nested groups included.

Reference

On this page