| name | fastify-server |
| description | Comprehensive guide for Fastify web framework development. This skill should be used when creating HTTP/REST APIs with Fastify, implementing route handlers, working with plugins and hooks, handling request validation, or integrating Fastify servers into Node-RED nodes. Covers server setup, routing patterns, plugin architecture, lifecycle hooks, error handling, and graceful shutdown. |
Fastify Server
Overview
Fastify is a high-performance, low-overhead web framework for Node.js focused on developer experience and plugin architecture. This skill provides patterns for building robust HTTP servers with Fastify, with emphasis on integration scenarios such as embedding Fastify within Node-RED nodes.
Quick Reference
| Task | Pattern |
|---|
| Create server | fastify({ logger: true }) |
| Define route | fastify.get('/path', handler) |
| Add plugin | fastify.register(plugin, options) |
| Validate request | Use JSON Schema in route options |
| Handle errors | fastify.setErrorHandler(handler) |
| Graceful shutdown | fastify.close() returns Promise |
Server Setup and Configuration
Basic Server Creation
const fastify = require('fastify');
const server = fastify({
logger: true,
ignoreTrailingSlash: true,
caseSensitive: false,
requestIdHeader: 'x-request-id',
trustProxy: true,
maxParamLength: 100,
});
Configuration Options
const server = fastify({
logger: {
level: 'info',
transport: {
target: 'pino-pretty',
options: { colorize: true }
}
},
connectionTimeout: 0,
keepAliveTimeout: 72000,
bodyLimit: 1048576,
http2: false,
https: null,
disableRequestLogging: false,
requestIdLogLabel: 'reqId',
genReqId: (req) => nanoid(),
});
Starting and Stopping the Server
await server.listen({ port: 3000, host: '0.0.0.0' });
const address = server.server.address();
console.log(`Server listening on ${address.port}`);
await server.close();
Route Definition
HTTP Methods
fastify.get('/users', getUsers);
fastify.post('/users', createUser);
fastify.put('/users/:id', updateUser);
fastify.patch('/users/:id', patchUser);
fastify.delete('/users/:id', deleteUser);
fastify.head('/users', headUsers);
fastify.options('/users', optionsUsers);
fastify.all('/webhook', webhookHandler);
fastify.get('/users/:id', {
schema: { },
preHandler: [authMiddleware],
}, async (request, reply) => {
return { user: request.params.id };
});
URL Parameters and Query Strings
fastify.get('/users/:userId/posts/:postId', async (request, reply) => {
const { userId, postId } = request.params;
return { userId, postId };
});
fastify.get('/files/*', async (request, reply) => {
const filePath = request.params['*'];
return { path: filePath };
});
fastify.get('/search', async (request, reply) => {
const { q, page = 1 } = request.query;
return { query: q, page: parseInt(page) };
});
Request Body
fastify.post('/users', async (request, reply) => {
const { name, email } = request.body;
return { created: { name, email } };
});
const server = fastify({
bodyLimit: 1048576,
});
server.addContentTypeParser('text/plain', { parseAs: 'string' }, (req, body, done) => {
done(null, body);
});
Response Handling
fastify.get('/example', async (request, reply) => {
return { data: 'value' };
});
fastify.get('/manual', async (request, reply) => {
reply.code(201);
reply.header('X-Custom', 'value');
reply.type('application/json');
return { created: true };
});
fastify.get('/old', async (request, reply) => {
return reply.redirect('/new');
});
fastify.get('/html', async (request, reply) => {
reply.type('text/html');
return '<h1>Hello</h1>';
});
fastify.get('/stream', async (request, reply) => {
const stream = fs.createReadStream('file.txt');
return reply.send(stream);
});
Route Prefix and Organization
fastify.register(async function userRoutes(fastify, opts) {
fastify.get('/users', listUsers);
fastify.post('/users', createUser);
}, { prefix: '/api/v1' });
module.exports = async function(fastify, opts) {
fastify.get('/', listUsers);
fastify.get('/:id', getUser);
};
fastify.register(require('./routes/users'), { prefix: '/users' });
Plugin Architecture
Creating Plugins
async function myPlugin(fastify, options) {
fastify.decorate('myUtil', () => 'utility function');
fastify.addHook('onRequest', async (request, reply) => {
request.customProperty = 'value';
});
}
fastify.register(myPlugin, { option1: 'value' });
const fp = require('fastify-plugin');
module.exports = fp(async function(fastify, opts) {
fastify.decorate('sharedUtil', () => 'available everywhere');
}, {
name: 'my-shared-plugin',
fastify: '4.x',
});
Encapsulation
fastify.register(async function(fastify) {
fastify.decorate('localUtil', () => {});
fastify.get('/local', async (req, reply) => {
return fastify.localUtil();
});
});
Decorators
fastify.decorate('db', databaseConnection);
fastify.decorate('config', { apiKey: 'xxx' });
fastify.decorateRequest('user', null);
fastify.addHook('onRequest', async (request) => {
request.user = await getUser(request.headers.authorization);
});
fastify.decorateReply('sendError', function(code, message) {
this.code(code).send({ error: message });
});
fastify.get('/profile', async (request, reply) => {
if (!request.user) {
return reply.sendError(401, 'Unauthorized');
}
return { user: request.user };
});
Common Plugins
await fastify.register(require('@fastify/cors'), {
origin: ['http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
});
await fastify.register(require('@fastify/helmet'));
await fastify.register(require('@fastify/rate-limit'), {
max: 100,
timeWindow: '1 minute',
});
await fastify.register(require('@fastify/jwt'), {
secret: 'supersecret',
});
await fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/public/',
});
Lifecycle Hooks
Request Lifecycle Order
onRequest → preParsing → preValidation → preHandler → handler → preSerialization → onSend → onResponse
Hook Implementations
fastify.addHook('onRequest', async (request, reply) => {
request.startTime = Date.now();
});
fastify.addHook('preParsing', async (request, reply, payload) => {
return payload;
});
fastify.addHook('preValidation', async (request, reply) => {
});
fastify.addHook('preHandler', async (request, reply) => {
if (!request.headers.authorization) {
reply.code(401).send({ error: 'Unauthorized' });
return;
}
});
fastify.addHook('preSerialization', async (request, reply, payload) => {
return { ...payload, timestamp: Date.now() };
});
fastify.addHook('onSend', async (request, reply, payload) => {
return payload;
});
fastify.addHook('onResponse', async (request, reply) => {
const duration = Date.now() - request.startTime;
request.log.info({ duration }, 'Request completed');
});
fastify.addHook('onError', async (request, reply, error) => {
request.log.error(error);
});
Route-Level Hooks
fastify.get('/protected', {
preHandler: async (request, reply) => {
await verifyToken(request);
},
preSerialization: async (request, reply, payload) => {
return sanitizeResponse(payload);
},
}, async (request, reply) => {
return { data: 'protected' };
});
Application Lifecycle Hooks
fastify.addHook('onReady', async () => {
await database.connect();
});
fastify.addHook('onClose', async (instance) => {
await database.disconnect();
});
fastify.addHook('onListen', async () => {
console.log('Server is now listening');
});
fastify.addHook('onRoute', (routeOptions) => {
console.log(`Route registered: ${routeOptions.method} ${routeOptions.url}`);
});
fastify.addHook('onRegister', (instance, opts) => {
console.log('Plugin registered with prefix:', opts.prefix);
});
Error Handling
Custom Error Handler
fastify.setErrorHandler(async (error, request, reply) => {
request.log.error(error);
if (error.validation) {
return reply.code(400).send({
error: 'Validation Error',
message: error.message,
details: error.validation,
});
}
if (error.statusCode) {
return reply.code(error.statusCode).send({
error: error.name,
message: error.message,
});
}
return reply.code(500).send({
error: 'Internal Server Error',
message: 'An unexpected error occurred',
});
});
Creating Custom Errors
const createError = require('@fastify/error');
const NotFoundError = createError('NOT_FOUND', 'Resource %s not found', 404);
const UnauthorizedError = createError('UNAUTHORIZED', 'Authentication required', 401);
const ForbiddenError = createError('FORBIDDEN', 'Access denied to %s', 403);
fastify.get('/users/:id', async (request, reply) => {
const user = await db.findUser(request.params.id);
if (!user) {
throw new NotFoundError(request.params.id);
}
return user;
});
Not Found Handler
fastify.setNotFoundHandler({
preHandler: async (request, reply) => {
},
}, async (request, reply) => {
return reply.code(404).send({
error: 'Not Found',
message: `Route ${request.method} ${request.url} not found`,
});
});
Schema Validation
JSON Schema Validation
const userSchema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
role: { type: 'string', enum: ['user', 'admin', 'moderator'] },
},
additionalProperties: false,
};
fastify.post('/users', {
schema: {
body: userSchema,
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
},
},
}, async (request, reply) => {
const user = await createUser(request.body);
reply.code(201);
return user;
});
Route Schema Options
fastify.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string', pattern: '^[0-9a-f]{24}$' },
},
},
querystring: {
type: 'object',
properties: {
include: { type: 'string', enum: ['posts', 'comments'] },
},
},
headers: {
type: 'object',
properties: {
'x-api-key': { type: 'string' },
},
required: ['x-api-key'],
},
response: {
200: { },
404: { },
},
},
}, handler);
TypeBox for Type-Safe Schemas
See references/typebox-schemas.md for comprehensive TypeBox examples and patterns.
const { Type } = require('@sinclair/typebox');
const UserSchema = Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' }),
age: Type.Optional(Type.Integer({ minimum: 0 })),
});
fastify.post('/users', {
schema: {
body: UserSchema,
},
}, async (request, reply) => {
return { created: request.body };
});
Node-RED Integration
Embedding Fastify in a Node-RED Node
module.exports = function(RED) {
function FastifyServerNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const fastify = require('fastify')({
logger: {
level: config.logLevel || 'info',
stream: {
write: (msg) => {
const parsed = JSON.parse(msg);
node.log(parsed.msg);
},
},
},
});
node.server = fastify;
const registeredRoutes = new Set();
node.addRoute = function(method, path, handler) {
const routeKey = `${method.toUpperCase()} ${path}`;
if (!registeredRoutes.has(routeKey)) {
fastify[method.toLowerCase()](path, handler);
registeredRoutes.add(routeKey);
}
};
async function startServer() {
try {
await fastify.listen({
port: config.port || 3000,
host: config.host || '0.0.0.0',
});
node.status({ fill: 'green', shape: 'dot', text: `listening on ${config.port}` });
} catch (err) {
node.error(`Failed to start server: ${err.message}`);
node.status({ fill: 'red', shape: 'ring', text: 'error' });
}
}
startServer();
node.on('close', async function(done) {
node.status({ fill: 'yellow', shape: 'ring', text: 'closing' });
try {
await fastify.close();
node.status({});
} catch (err) {
node.error(`Error during shutdown: ${err.message}`);
}
done();
});
}
RED.nodes.registerType('fastify-server', FastifyServerNode);
};
Route Node Pattern
module.exports = function(RED) {
function FastifyRouteNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const serverNode = RED.nodes.getNode(config.server);
if (!serverNode) {
node.error('No server configuration');
return;
}
const method = config.method || 'get';
const path = config.path || '/';
serverNode.addRoute(method, path, async (request, reply) => {
const msg = {
payload: request.body || {},
req: {
params: request.params,
query: request.query,
headers: request.headers,
method: request.method,
url: request.url,
},
_fastifyReply: reply,
};
node.send(msg);
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Response timeout'));
}, config.timeout || 30000);
msg._resolve = (response) => {
clearTimeout(timeout);
resolve(response);
};
msg._reject = (error) => {
clearTimeout(timeout);
reject(error);
};
});
});
node.status({ fill: 'green', shape: 'dot', text: `${method.toUpperCase()} ${path}` });
}
RED.nodes.registerType('fastify-route', FastifyRouteNode);
};
Response Node Pattern
module.exports = function(RED) {
function FastifyResponseNode(config) {
RED.nodes.createNode(this, config);
const node = this;
node.on('input', function(msg) {
if (msg._resolve) {
const statusCode = msg.statusCode || 200;
const headers = msg.headers || {};
if (msg._fastifyReply) {
msg._fastifyReply.code(statusCode);
Object.entries(headers).forEach(([key, value]) => {
msg._fastifyReply.header(key, value);
});
}
msg._resolve(msg.payload);
} else {
node.warn('No pending request to respond to');
}
});
}
RED.nodes.registerType('fastify-response', FastifyResponseNode);
};
Graceful Shutdown Patterns
node.on('close', async function(removed, done) {
const shutdownTimeout = 10000;
const timer = setTimeout(() => {
node.warn('Shutdown timed out, forcing close');
done();
}, shutdownTimeout);
try {
node.status({ fill: 'yellow', shape: 'ring', text: 'draining' });
await fastify.close();
clearTimeout(timer);
node.status({});
} catch (err) {
clearTimeout(timer);
node.error(`Shutdown error: ${err.message}`);
}
done();
});
process.on('SIGTERM', async () => {
await fastify.close();
process.exit(0);
});
Testing Fastify Nodes
const helper = require('node-red-node-test-helper');
const fastifyServerNode = require('../nodes/fastify-server');
const fastifyRouteNode = require('../nodes/fastify-route');
describe('Fastify Server Node', function() {
beforeEach(function(done) {
helper.startServer(done);
});
afterEach(function(done) {
helper.unload().then(() => helper.stopServer(done));
});
it('should start server on configured port', async function() {
const flow = [
{ id: 'server1', type: 'fastify-server', port: 3456 },
];
await helper.load([fastifyServerNode], flow);
const serverNode = helper.getNode('server1');
await new Promise(resolve => setTimeout(resolve, 100));
const response = await fetch('http://localhost:3456/health');
expect(response.ok).to.be.true;
});
it('should handle routes and send messages', async function() {
const flow = [
{ id: 'server1', type: 'fastify-server', port: 3457 },
{ id: 'route1', type: 'fastify-route', server: 'server1', method: 'get', path: '/test', wires: [['helper1']] },
{ id: 'helper1', type: 'helper' },
];
await helper.load([fastifyServerNode, fastifyRouteNode], flow);
const helperNode = helper.getNode('helper1');
const msgPromise = new Promise(resolve => {
helperNode.on('input', resolve);
});
fetch('http://localhost:3457/test');
const msg = await msgPromise;
expect(msg.req.method).to.equal('GET');
expect(msg.req.url).to.equal('/test');
});
});
Resources
References
references/typebox-schemas.md - Comprehensive TypeBox schema examples and validation patterns
External Documentation