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
Fastify executes hooks in a specific order:
Incoming Request
|
onRequest
|
preParsing
|
preValidation
|
preHandler
|
Handler
|
preSerialization
|
onSend
|
onResponseFirst hook to execute, before body parsing. Use for authentication, request ID setup:
import Fastify from 'fastify';
const app = Fastify();
// Global onRequest hook
app.addHook('onRequest', async (request, reply) => {
request.startTime = Date.now();
request.log.info({ url: request.url, method: request.method }, 'Request started');
});
// Authentication check
app.addHook('onRequest', async (request, reply) => {
// Skip auth for public routes
if (request.url.startsWith('/public')) {
return;
}
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
reply.code(401).send({ error: 'Unauthorized' });
return; // Stop processing
}
try {
request.user = await verifyToken(token);
} catch {
reply.code(401).send({ error: 'Invalid token' });
}
});Execute before body parsing. Can modify the payload stream:
app.addHook('preParsing', async (request, reply, payload) => {
// Log raw payload size
request.log.debug({ contentLength: request.headers['content-length'] }, 'Parsing body');
// Return modified payload stream if needed
return payload;
});
// Decompress incoming data
app.addHook('preParsing', async (request, reply, payload) => {
if (request.headers['content-encoding'] === 'gzip') {
return payload.pipe(zlib.createGunzip());
}
return payload;
});Execute after parsing, before schema validation:
app.addHook('preValidation', async (request, reply) => {
// Modify body before validation
if (request.body && typeof request.body === 'object') {
// Normalize data
request.body.email = request.body.email?.toLowerCase().trim();
}
});
// Rate limiting check
app.addHook('preValidation', async (request, reply) => {
const key = request.ip;
const count = await redis.incr(`ratelimit:${key}`);
if (count === 1) {
await redis.expire(`ratelimit:${key}`, 60);
}
if (count > 100) {
reply.code(429).send({ error: 'Too many requests' });
}
});Most common hook, execute after validation, before handler:
// Authorization check
app.addHook('preHandler', async (request, reply) => {
const { userId } = request.params as { userId: string };
if (request.user.id !== userId && !request.user.isAdmin) {
reply.code(403).send({ error: 'Forbidden' });
}
});
// Load related data
app.addHook('preHandler', async (request, reply) => {
if (request.params?.projectId) {
request.project = await db.projects.findById(request.params.projectId);
if (!request.project) {
reply.code(404).send({ error: 'Project not found' });
}
}
});
// Transaction wrapper
app.addHook('preHandler', async (request) => {
request.transaction = await db.beginTransaction();
});
app.addHook('onResponse', async (request) => {
if (request.transaction) {
await request.transaction.commit();
}
});
app.addHook('onError', async (request, reply, error) => {
if (request.transaction) {
await request.transaction.rollback();
}
});Modify payload before serialization:
app.addHook('preSerialization', async (request, reply, payload) => {
// Add metadata to all responses
if (payload && typeof payload === 'object') {
return {
...payload,
_meta: {
requestId: request.id,
timestamp: new Date().toISOString(),
},
};
}
return payload;
});
// Remove sensitive fields
app.addHook('preSerialization', async (request, reply, payload) => {
if (payload?.user?.password) {
const { password, ...user } = payload.user;
return { ...payload, user };
}
return payload;
});Modify response after serialization:
app.addHook('onSend', async (request, reply, payload) => {
// Add response headers
reply.header('X-Response-Time', Date.now() - request.startTime);
// Compress response
if (payload && payload.length > 1024) {
const compressed = await gzip(payload);
reply.header('Content-Encoding', 'gzip');
return compressed;
}
return payload;
});
// Transform JSON string response
app.addHook('onSend', async (request, reply, payload) => {
if (reply.getHeader('content-type')?.includes('application/json')) {
// payload is already a string at this point
return payload;
}
return payload;
});Execute after response is sent. Cannot modify response:
app.addHook('onResponse', async (request, reply) => {
// Log response time
const responseTime = Date.now() - request.startTime;
request.log.info({
method: request.method,
url: request.url,
statusCode: reply.statusCode,
responseTime,
}, 'Request completed');
// Track metrics
metrics.histogram('http_request_duration', responseTime, {
method: request.method,
route: request.routeOptions.url,
status: reply.statusCode,
});
});Execute when an error is thrown:
app.addHook('onError', async (request, reply, error) => {
// Log error details
request.log.error({
err: error,
url: request.url,
method: request.method,
body: request.body,
}, 'Request error');
// Track error metrics
metrics.increment('http_errors', {
error: error.code || 'UNKNOWN',
route: request.routeOptions.url,
});
// Cleanup resources
if (request.tempFile) {
await fs.unlink(request.tempFile).catch(() => {});
}
});Execute when request times out:
const app = Fastify({
connectionTimeout: 30000, // 30 seconds
});
app.addHook('onTimeout', async (request, reply) => {
request.log.warn({
url: request.url,
method: request.method,
}, 'Request timeout');
// Cleanup
if (request.abortController) {
request.abortController.abort();
}
});Execute when client closes connection:
app.addHook('onRequestAbort', async (request) => {
request.log.info('Client aborted request');
// Cancel ongoing operations
if (request.abortController) {
request.abortController.abort();
}
// Cleanup uploaded files
if (request.uploadedFiles) {
for (const file of request.uploadedFiles) {
await fs.unlink(file.path).catch(() => {});
}
}
});Hooks that run at application startup/shutdown:
// After all plugins are loaded
app.addHook('onReady', async function () {
this.log.info('Server is ready');
// Initialize connections
await this.db.connect();
await this.redis.connect();
// Warm caches
await this.cache.warmup();
});
// When server is closing
app.addHook('onClose', async function () {
this.log.info('Server is closing');
// Cleanup connections
await this.db.close();
await this.redis.disconnect();
});
// After routes are registered
app.addHook('onRoute', (routeOptions) => {
console.log(`Route registered: ${routeOptions.method} ${routeOptions.url}`);
// Track all routes
routes.push({
method: routeOptions.method,
url: routeOptions.url,
schema: routeOptions.schema,
});
});
// After plugin is registered
app.addHook('onRegister', (instance, options) => {
console.log(`Plugin registered with prefix: ${options.prefix}`);
});Hooks are scoped to their encapsulation context:
app.addHook('onRequest', async (request) => {
// Runs for ALL routes
request.log.info('Global hook');
});
app.register(async function adminRoutes(fastify) {
// Only runs for routes in this plugin
fastify.addHook('onRequest', async (request, reply) => {
if (!request.user?.isAdmin) {
reply.code(403).send({ error: 'Admin only' });
}
});
fastify.get('/admin/users', async () => {
return { users: [] };
});
}, { prefix: '/admin' });Multiple hooks of the same type execute in registration order:
app.addHook('onRequest', async () => {
console.log('First');
});
app.addHook('onRequest', async () => {
console.log('Second');
});
app.addHook('onRequest', async () => {
console.log('Third');
});
// Output: First, Second, ThirdReturn early from hooks to stop processing:
app.addHook('preHandler', async (request, reply) => {
if (!request.user) {
// Send response and return to stop further processing
reply.code(401).send({ error: 'Unauthorized' });
return;
}
// Continue to next hook and handler
});Add hooks to specific routes:
const adminOnlyHook = async (request, reply) => {
if (!request.user?.isAdmin) {
reply.code(403).send({ error: 'Forbidden' });
}
};
app.get('/admin/settings', {
preHandler: [adminOnlyHook],
handler: async (request) => {
return { settings: {} };
},
});
// Multiple hooks
app.post('/orders', {
preValidation: [validateApiKey],
preHandler: [loadUser, checkQuota, logOrder],
handler: createOrderHandler,
});Always use async/await in hooks:
// GOOD - async hook
app.addHook('preHandler', async (request, reply) => {
const user = await loadUser(request.headers.authorization);
request.user = user;
});
// AVOID - callback style (deprecated)
app.addHook('preHandler', (request, reply, done) => {
loadUser(request.headers.authorization)
.then((user) => {
request.user = user;
done();
})
.catch(done);
});