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 fastifybun add @kizunajs/fastify@beta fastifynpm install @kizunajs/fastify@beta fastifyThe 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
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
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:
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.
.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.
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
await kizuna.api.mount(app, {
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. |
The same options go on fastifyKizuna when you register the plugin yourself:
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.
declare module 'fastify' {
interface FastifyRequest {
requestId: string;
}
}Both middleware and handlers are then typed:
.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.