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 fastifybun add @ts-kizuna/fastify fastifynpm install @ts-kizuna/fastify fastifySetup
The standard setup:
new KizunaServer()binds the contract to aserverhandleserver.routerbinds typed implementations to the contractserver.apicombines the router and guards into an API objectfastifyKizunais the Fastify plugin that mounts the API
import { KizunaServer } from '@ts-kizuna/fastify';
import { contract } from '@shared/contract';
export const server = new KizunaServer(contract);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,
};
},
},
});import { server } from './server';
import { router } from './router';
export const api = server.api({
router,
});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.
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,
});| Option | Default | Description |
|---|---|---|
responseValidation | false | Validate handler return values against response schemas. Enable in development. |
formatError | none | Reshape 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:
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.