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
Define response schemas with TypeBox for automatic TypeScript types and fast serialization:
import Fastify from 'fastify';
import { Type, type Static } from '@sinclair/typebox';
const app = Fastify();
// Define response schema with TypeBox
const UserResponse = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String(),
});
const UsersResponse = Type.Array(UserResponse);
type UserResponseType = Static<typeof UserResponse>;
// With TypeBox schema - uses fast-json-stringify (faster) + TypeScript types
app.get<{ Reply: Static<typeof UsersResponse> }>('/users', {
schema: {
response: {
200: UsersResponse,
},
},
}, async () => {
return db.users.findAll();
});
// Without schema - uses JSON.stringify (slower), no type safety
app.get('/users-slow', async () => {
return db.users.findAll();
});Fastify uses fast-json-stringify when response schemas are defined. This provides:
app.get('/user/:id', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
// password is NOT in schema, so it's stripped
},
},
},
},
}, async (request) => {
const user = await db.users.findById(request.params.id);
// Even if user has password field, it won't be serialized
return user;
});Define schemas for different response codes:
app.get('/users/:id', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
404: {
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
},
},
},
},
}, async (request, reply) => {
const user = await db.users.findById(request.params.id);
if (!user) {
reply.code(404);
return { statusCode: 404, error: 'Not Found', message: 'User not found' };
}
return user;
});Use 'default' for common error responses:
app.get('/resource', {
schema: {
response: {
200: { $ref: 'resource#' },
'4xx': {
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
},
},
'5xx': {
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
},
},
},
},
}, handler);Create custom serialization functions:
// Per-route serializer
app.get('/custom', {
schema: {
response: {
200: {
type: 'object',
properties: {
value: { type: 'string' },
},
},
},
},
serializerCompiler: ({ schema }) => {
return (data) => {
// Custom serialization logic
return JSON.stringify({
value: String(data.value).toUpperCase(),
serializedAt: new Date().toISOString(),
});
};
},
}, async () => {
return { value: 'hello' };
});Use the global serializer compiler:
import Fastify from 'fastify';
const app = Fastify({
serializerCompiler: ({ schema, method, url, httpStatus }) => {
// Custom compilation logic
const stringify = fastJson(schema);
return (data) => stringify(data);
},
});fast-json-stringify coerces types:
app.get('/data', {
schema: {
response: {
200: {
type: 'object',
properties: {
count: { type: 'integer' }, // '5' -> 5
active: { type: 'boolean' }, // 'true' -> true
tags: {
type: 'array',
items: { type: 'string' }, // [1, 2] -> ['1', '2']
},
},
},
},
},
}, async () => {
return {
count: '5', // Coerced to integer
active: 'true', // Coerced to boolean
tags: [1, 2, 3], // Coerced to strings
};
});Handle nullable fields properly:
app.get('/profile', {
schema: {
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
bio: { type: ['string', 'null'] },
avatar: {
oneOf: [
{ type: 'string', format: 'uri' },
{ type: 'null' },
],
},
},
},
},
},
}, async () => {
return {
name: 'John',
bio: null,
avatar: null,
};
});Control extra properties in response:
// Strip additional properties (default)
app.get('/strict', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
additionalProperties: false,
},
},
},
}, async () => {
return { id: '1', name: 'John', secret: 'hidden' };
// Output: { "id": "1", "name": "John" }
});
// Allow additional properties
app.get('/flexible', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
},
additionalProperties: true,
},
},
},
}, async () => {
return { id: '1', extra: 'included' };
// Output: { "id": "1", "extra": "included" }
});Serialize nested structures:
app.addSchema({
$id: 'address',
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
country: { type: 'string' },
},
});
app.get('/user', {
schema: {
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
address: { $ref: 'address#' },
contacts: {
type: 'array',
items: {
type: 'object',
properties: {
type: { type: 'string' },
value: { type: 'string' },
},
},
},
},
},
},
},
}, async () => {
return {
name: 'John',
address: { street: '123 Main', city: 'Boston', country: 'USA' },
contacts: [
{ type: 'email', value: 'john@example.com' },
{ type: 'phone', value: '+1234567890' },
],
};
});Handle dates consistently:
app.get('/events', {
schema: {
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
date: { type: 'string', format: 'date-time' },
},
},
},
},
},
}, async () => {
const events = await db.events.findAll();
// Convert Date objects to ISO strings
return events.map((e) => ({
...e,
date: e.date.toISOString(),
}));
});Handle BigInt values:
// BigInt is not JSON serializable by default
app.get('/large-number', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' }, // Serialize as string
count: { type: 'integer' },
},
},
},
},
}, async () => {
const bigValue = 9007199254740993n;
return {
id: bigValue.toString(), // Convert to string
count: Number(bigValue), // Or number if safe
};
});Stream responses bypass serialization:
import { createReadStream } from 'node:fs';
app.get('/file', async (request, reply) => {
const stream = createReadStream('./data.json');
reply.type('application/json');
return reply.send(stream);
});
// Streaming JSON array
app.get('/stream', async (request, reply) => {
reply.type('application/json');
const cursor = db.users.findCursor();
reply.raw.write('[');
let first = true;
for await (const user of cursor) {
if (!first) reply.raw.write(',');
reply.raw.write(JSON.stringify(user));
first = false;
}
reply.raw.write(']');
reply.raw.end();
});Modify data before serialization:
app.addHook('preSerialization', async (request, reply, payload) => {
// Add metadata to responses
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
return {
...payload,
_links: {
self: request.url,
},
};
}
return payload;
});Skip serialization for specific routes:
app.get('/raw', async (request, reply) => {
const data = JSON.stringify({ raw: true });
reply.type('application/json');
reply.serializer((payload) => payload); // Pass through
return data;
});