Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts.
95
95%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Prefer TypeBox for defining schemas. It provides TypeScript types automatically and compiles to JSON Schema:
import Fastify from 'fastify';
import { Type, type Static } from '@sinclair/typebox';
const app = Fastify();
// Define schema with TypeBox - get TypeScript types for free
const CreateUserBody = Type.Object({
name: Type.String({ minLength: 1, maxLength: 100 }),
email: Type.String({ format: 'email' }),
age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
});
const UserResponse = Type.Object({
id: Type.String({ format: 'uuid' }),
name: Type.String(),
email: Type.String(),
createdAt: Type.String({ format: 'date-time' }),
});
// TypeScript types are derived automatically
type CreateUserBodyType = Static<typeof CreateUserBody>;
type UserResponseType = Static<typeof UserResponse>;
app.post<{
Body: CreateUserBodyType;
Reply: UserResponseType;
}>('/users', {
schema: {
body: CreateUserBody,
response: {
201: UserResponse,
},
},
}, async (request, reply) => {
// request.body is fully typed as CreateUserBodyType
const user = await createUser(request.body);
reply.code(201);
return user;
});import { Type, type Static } from '@sinclair/typebox';
// Enums
const Status = Type.Union([
Type.Literal('active'),
Type.Literal('inactive'),
Type.Literal('pending'),
]);
// Arrays
const Tags = Type.Array(Type.String(), { minItems: 1, maxItems: 10 });
// Nested objects
const Address = Type.Object({
street: Type.String(),
city: Type.String(),
country: Type.String(),
zip: Type.Optional(Type.String()),
});
// References (reusable schemas)
const User = Type.Object({
id: Type.String({ format: 'uuid' }),
name: Type.String(),
address: Address,
tags: Tags,
status: Status,
});
// Nullable
const NullableString = Type.Union([Type.String(), Type.Null()]);
// Record/Map
const Metadata = Type.Record(Type.String(), Type.Unknown());import { Type, type Static } from '@sinclair/typebox';
// Define shared schemas
const ErrorResponse = Type.Object({
error: Type.String(),
message: Type.String(),
statusCode: Type.Integer(),
});
const PaginationQuery = Type.Object({
page: Type.Integer({ minimum: 1, default: 1 }),
limit: Type.Integer({ minimum: 1, maximum: 100, default: 20 }),
});
// Register globally
app.addSchema(Type.Object({ $id: 'ErrorResponse', ...ErrorResponse }));
app.addSchema(Type.Object({ $id: 'PaginationQuery', ...PaginationQuery }));
// Reference in routes
app.get('/items', {
schema: {
querystring: { $ref: 'PaginationQuery#' },
response: {
400: { $ref: 'ErrorResponse#' },
},
},
}, handler);You can also use plain JSON Schema directly:
import Fastify from 'fastify';
const app = Fastify();
const createUserSchema = {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
},
required: ['name', 'email'],
additionalProperties: false,
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
email: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' },
},
},
},
};
app.post('/users', { schema: createUserSchema }, async (request, reply) => {
const user = await createUser(request.body);
reply.code(201);
return user;
});Validate different parts of the request:
const fullRequestSchema = {
// URL parameters
params: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
},
required: ['id'],
},
// Query string
querystring: {
type: 'object',
properties: {
include: { type: 'string', enum: ['posts', 'comments', 'all'] },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
},
// Request headers
headers: {
type: 'object',
properties: {
'x-api-key': { type: 'string', minLength: 32 },
},
required: ['x-api-key'],
},
// Request body
body: {
type: 'object',
properties: {
data: { type: 'object' },
},
required: ['data'],
},
};
app.put('/resources/:id', { schema: fullRequestSchema }, handler);Define reusable schemas with $id and reference them with $ref:
// Add shared schemas to Fastify
app.addSchema({
$id: 'user',
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
createdAt: { type: 'string', format: 'date-time' },
},
required: ['id', 'name', 'email'],
});
app.addSchema({
$id: 'userCreate',
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
additionalProperties: false,
});
app.addSchema({
$id: 'error',
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
},
});
// Reference shared schemas
app.post('/users', {
schema: {
body: { $ref: 'userCreate#' },
response: {
201: { $ref: 'user#' },
400: { $ref: 'error#' },
},
},
}, handler);
app.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: { id: { type: 'string', format: 'uuid' } },
required: ['id'],
},
response: {
200: { $ref: 'user#' },
404: { $ref: 'error#' },
},
},
}, handler);Define schemas for array responses:
app.addSchema({
$id: 'userList',
type: 'object',
properties: {
users: {
type: 'array',
items: { $ref: 'user#' },
},
total: { type: 'integer' },
page: { type: 'integer' },
pageSize: { type: 'integer' },
},
});
app.get('/users', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer', minimum: 1, default: 1 },
pageSize: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
},
},
response: {
200: { $ref: 'userList#' },
},
},
}, handler);Add custom validation formats:
import Fastify from 'fastify';
const app = Fastify({
ajv: {
customOptions: {
formats: {
'iso-country': /^[A-Z]{2}$/,
'phone': /^\+?[1-9]\d{1,14}$/,
},
},
},
});
// Or add formats dynamically
app.addSchema({
$id: 'address',
type: 'object',
properties: {
street: { type: 'string' },
country: { type: 'string', format: 'iso-country' },
phone: { type: 'string', format: 'phone' },
},
});Add custom validation keywords:
import Fastify from 'fastify';
import Ajv from 'ajv';
const app = Fastify({
ajv: {
customOptions: {
keywords: [
{
keyword: 'isEven',
type: 'number',
validate: (schema: boolean, data: number) => {
if (schema) {
return data % 2 === 0;
}
return true;
},
errors: false,
},
],
},
},
});
// Use custom keyword
app.post('/numbers', {
schema: {
body: {
type: 'object',
properties: {
value: { type: 'integer', isEven: true },
},
},
},
}, handler);Fastify coerces types by default for query strings and params:
// Query string "?page=5&active=true" becomes:
// { page: 5, active: true } (number and boolean, not strings)
app.get('/items', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer' }, // "5" -> 5
active: { type: 'boolean' }, // "true" -> true
tags: {
type: 'array',
items: { type: 'string' }, // "a,b,c" -> ["a", "b", "c"]
},
},
},
},
}, handler);Customize validation error responses:
app.setErrorHandler((error, request, reply) => {
if (error.validation) {
reply.code(400).send({
error: 'Validation Error',
message: 'Request validation failed',
details: error.validation.map((err) => ({
field: err.instancePath || err.params?.missingProperty,
message: err.message,
keyword: err.keyword,
})),
});
return;
}
// Handle other errors
reply.code(error.statusCode || 500).send({
error: error.name,
message: error.message,
});
});Configure the Ajv schema compiler:
import Fastify from 'fastify';
const app = Fastify({
ajv: {
customOptions: {
removeAdditional: 'all', // Remove extra properties
useDefaults: true, // Apply default values
coerceTypes: true, // Coerce types
allErrors: true, // Report all errors, not just first
},
plugins: [
require('ajv-formats'), // Add format validators
],
},
});Handle nullable fields properly:
app.addSchema({
$id: 'profile',
type: 'object',
properties: {
name: { type: 'string' },
bio: { type: ['string', 'null'] }, // Can be string or null
avatar: {
oneOf: [
{ type: 'string', format: 'uri' },
{ type: 'null' },
],
},
},
});Use if/then/else for conditional validation:
app.addSchema({
$id: 'payment',
type: 'object',
properties: {
method: { type: 'string', enum: ['card', 'bank'] },
cardNumber: { type: 'string' },
bankAccount: { type: 'string' },
},
required: ['method'],
if: {
properties: { method: { const: 'card' } },
},
then: {
required: ['cardNumber'],
},
else: {
required: ['bankAccount'],
},
});Organize schemas in a dedicated file:
// schemas/index.ts
export const schemas = [
{
$id: 'user',
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
},
{
$id: 'error',
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
},
},
];
// app.ts
import { schemas } from './schemas/index.js';
for (const schema of schemas) {
app.addSchema(schema);
}Schemas work directly with @fastify/swagger:
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUi from '@fastify/swagger-ui';
app.register(fastifySwagger, {
openapi: {
info: {
title: 'My API',
version: '1.0.0',
},
},
});
app.register(fastifySwaggerUi, {
routePrefix: '/docs',
});
// Schemas are automatically converted to OpenAPI definitionsResponse schemas enable fast-json-stringify for serialization:
// With response schema - uses fast-json-stringify (faster)
app.get('/users', {
schema: {
response: {
200: {
type: 'array',
items: { $ref: 'user#' },
},
},
},
}, handler);
// Without response schema - uses JSON.stringify (slower)
app.get('/users-slow', handler);Always define response schemas for production APIs to benefit from optimized serialization.