Fastify is the fastest Node.js HTTP framework — 2-3x faster than Express due to its schema-based serialization and compiled JSON handling. TypeBox schemas define both runtime validation and TypeScript types in one declaration. The plugin system with fastify-plugin creates composable, encapsulated modules. Claude Code writes Fastify route handlers with full TypeScript generics, plugin wrappers, lifecycle hooks, and the schema definitions that give Fastify its speed advantage.
CLAUDE.md for Fastify Projects
## API Stack
- Fastify 5.x with TypeScript
- Schema: TypeBox for route schemas (one source of truth for types + validation)
- Auth: @fastify/jwt for Bearer token auth
- Database: @fastify/postgres (pg pool) or drizzle-orm
- Plugins: fastify-plugin for cross-scope decoration; scoped for route-level features
- Error handling: never throw Error — use httpErrors.unauthorized() etc.
- Validation: schema.body/params/querystring on every route — no raw req.body access
- Serialization: response schemas enable 2x faster JSON serialization
Server Setup
// src/server.ts
import Fastify from 'fastify';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
import { ordersRoutes } from './routes/orders';
import { authRoutes } from './routes/auth';
import { dbPlugin } from './plugins/db';
export function buildServer() {
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
},
}).withTypeProvider<TypeBoxTypeProvider>(); // Enables TypeBox inference
// Global plugins
app.register(cors, { origin: process.env.CORS_ORIGIN ?? false });
app.register(jwt, { secret: process.env.JWT_SECRET! });
app.register(dbPlugin); // Decorates app.db
// Routes (encapsulated by default — auth can be scoped)
app.register(authRoutes, { prefix: '/api/auth' });
app.register(ordersRoutes, { prefix: '/api/orders' });
// Global error handler
app.setErrorHandler((error, request, reply) => {
if (error.validation) {
return reply.status(400).send({
error: 'Validation Error',
message: error.message,
fields: error.validation,
});
}
app.log.error({ err: error, req: request.id }, 'Unhandled error');
return reply.status(error.statusCode ?? 500).send({
error: 'Internal Server Error',
});
});
return app;
}
// main.ts
const app = buildServer();
await app.listen({ port: parseInt(process.env.PORT ?? '3000'), host: '0.0.0.0' });
TypeBox Schemas and Routes
// src/routes/orders.ts
import { Type, type Static } from '@sinclair/typebox';
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
import createHttpError from 'http-errors';
// Schemas: define once, TypeScript types inferred automatically
const CreateOrderBody = Type.Object({
customerId: Type.String({ format: 'uuid' }),
items: Type.Array(Type.Object({
productId: Type.String({ format: 'uuid' }),
quantity: Type.Integer({ minimum: 1, maximum: 100 }),
unitPriceCents: Type.Integer({ minimum: 1 }),
}), { minItems: 1 }),
notes: Type.Optional(Type.String({ maxLength: 500 })),
});
const OrderParams = Type.Object({
orderId: Type.String({ format: 'uuid' }),
});
const OrderResponse = Type.Object({
id: Type.String(),
status: Type.String(),
totalCents: Type.Integer(),
createdAt: Type.String({ format: 'date-time' }),
});
type CreateOrderInput = Static<typeof CreateOrderBody>;
export const ordersRoutes: FastifyPluginAsyncTypebox = async (app) => {
// Decorate all routes in this scope with JWT verification
app.addHook('preHandler', app.authenticate);
app.post('/', {
schema: {
body: CreateOrderBody,
response: { 201: OrderResponse },
},
}, async (request, reply) => {
// request.body is fully typed as CreateOrderInput
const { customerId, items, notes } = request.body;
const totalCents = items.reduce(
(sum, item) => sum + item.quantity * item.unitPriceCents,
0
);
const order = await app.db.createOrder({ customerId, items, notes, totalCents });
return reply.status(201).send(order);
});
app.get('/:orderId', {
schema: {
params: OrderParams,
response: { 200: OrderResponse },
},
}, async (request, reply) => {
const order = await app.db.getOrder(request.params.orderId);
if (!order) throw createHttpError.NotFound('Order not found');
if (order.userId !== request.user.id && !request.user.isAdmin) {
throw createHttpError.Forbidden();
}
return order;
});
};
Auth Plugin with Decorator
// src/plugins/auth.ts
import fp from 'fastify-plugin';
import type { FastifyPluginAsync } from 'fastify';
// fp() makes decorations available outside the encapsulated scope
const authPlugin: FastifyPluginAsync = fp(async (app) => {
// Decorate request with user type
app.decorateRequest('user', null);
// Reusable preHandler hook
app.decorate('authenticate', async function(request, reply) {
try {
const payload = await request.jwtVerify<{ id: string; email: string; isAdmin: boolean }>();
request.user = payload;
} catch {
reply.code(401).send({ error: 'Unauthorized' });
}
});
});
export default authPlugin;
// Type augmentation
declare module 'fastify' {
interface FastifyInstance {
authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
}
interface FastifyRequest {
user: { id: string; email: string; isAdmin: boolean };
}
}
Lifecycle Hooks
// src/hooks/request-id.ts — lifecycle hooks for tracing/logging
import fp from 'fastify-plugin';
export default fp(async (app) => {
// onRequest: first hook in lifecycle
app.addHook('onRequest', async (request) => {
request.startTime = Date.now();
});
// onSend: called before response is sent — add headers
app.addHook('onSend', async (request, reply) => {
reply.header('X-Request-ID', request.id);
reply.header('X-Response-Time', `${Date.now() - request.startTime}ms`);
});
// onError: log response errors with context
app.addHook('onError', async (request, reply, error) => {
app.log.error({
reqId: request.id,
statusCode: reply.statusCode,
url: request.url,
method: request.method,
err: error.message,
}, 'Request error');
});
});
declare module 'fastify' {
interface FastifyRequest { startTime: number; }
}
For the Express.js comparison — when Fastify’s performance matters vs Express’s ecosystem size — the Node.js backend patterns guide covers REST API design conventions. For deploying Fastify on AWS Lambda with a 10ms cold start, the AWS Lambda guide covers Fastify Lambda adapters. The Claude Skills 360 bundle includes Fastify skill sets covering TypeBox schemas, plugin architecture, JWT auth decorators, and lifecycle hooks. Start with the free tier to try Fastify route generation.