Dynamic Routing
Route Parameters and Dynamic Routing
Dynamic routing allows variables in URLs, which Fastify parses and provides as part of the request object. Example:
const fastify = require('fastify')();
fastify.get('/:id', async (request, reply) => {
const id = request.params.id;
return { id: id, message: `You requested resource with ID: ${id}` };
});
fastify.listen({ port: 3000 }, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.log.info(`Server listening on ${fastify.server.address().port}`);
});Here, /:id defines a dynamic parameter. Fastify parses the URL segment as request.params.id.
Using Dynamic Parameters
Dynamic parameters are accessible via request.params and can be used for tasks like database queries or resource loading:
fastify.get('/:id', async (request, reply) => {
const id = request.params.id;
const resource = await loadResource(id); // Hypothetical database function
return resource;
});Complex Dynamic Routing
Fastify supports nested and multiple parameters:
fastify.get('/:category/:id', async (request, reply) => {
const category = request.params.category;
const id = request.params.id;
return { category: category, id: id };
});This matches URLs like /users/123 or /posts/456, parsing category and id.
Regular Expression Route Matching
Fastify supports regular expressions for complex URL matching, though they may impact performance:
const fastify = require('fastify')();
fastify.get(/\/example\/(\d+)\.png/, async (request, reply) => {
const fileId = request.params[0];
return { id: fileId, message: `You requested an example file with ID: ${fileId}` };
});
fastify.listen({ port: 3000 }, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.log.info(`Server listening on ${fastify.server.address().port}`);
});This matches URLs like /example/123.png, with the numeric segment as request.params[0].
Considerations for Dynamic and Regex Routing
- Performance: Regular expressions can be slower; prefer simple or parameterized routes when possible.
- Named Parameters: Use named parameters for easier access in handlers.
- Regex Capture Groups: Ensure capture groups are handled correctly, as Fastify provides them in
params.
Fastify Plugin System
Plugin Structure
A Fastify plugin is a function that takes a Fastify instance, options, and a callback. It can register routes, middleware, or perform initialization:
module.exports = function (fastify, opts, next) {
fastify.get('/plugin', (request, reply) => {
return { hello: 'from plugin' };
});
fastify.decorateReply('myCustomMethod', function (arg) {
console.log('Called custom method with argument:', arg);
});
next();
};Loading Plugins
Load plugins with fastify.register() during application startup:
const fastify = require('fastify')();
fastify.register(require('./my-plugin'));
fastify.listen({ port: 3000 }, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.log.info(`Server listening on ${fastify.server.address().port}`);
});Plugin Options and Dependencies
Plugins accept an options object and can depend on other plugins:
module.exports = function (fastify, opts, next) {
const prefix = opts.prefix || '/default';
fastify.get(`${prefix}/plugin`, (request, reply) => {
return { hello: 'from plugin' };
});
next();
};
fastify.register(require('./my-plugin'), { prefix: '/custom' });Plugin Lifecycle
Plugins can use fastify.after and fastify.ready hooks, triggered after all plugins load or when Fastify is ready:
module.exports = function (fastify, opts, next) {
fastify.after(() => {
console.log('All plugins are loaded!');
});
fastify.ready(next);
};Creating Custom Plugins
Define Plugin Function:
// my-plugin.js
module.exports = function (fastify, options, next) {
fastify.get('/my-plugin-route', (request, reply) => {
reply.send({ message: 'Hello from my custom plugin!' });
});
fastify.decorate('myCustomMethod', function (arg) {
console.log('Custom method called with:', arg);
});
next();
};Export Plugin: Ensure the plugin function is the module’s default export.
Using Custom Plugins:
const fastify = require('fastify')();
fastify.register(require('./my-plugin'), { prefix: '/api' });Using Official and Third-Party Plugins
Fastify’s community offers plugins for authentication, logging, and database integration:
Install Plugin:
npm install fastify-mongodbLoad Plugin:
fastify.register(require('fastify-mongodb'), {
url: 'mongodb://localhost:27017/mydb',
database: 'mydb'
});Use Plugin Features:
fastify.get('/users', async (request, reply) => {
const users = await fastify.db.collection('users').find().toArray();
reply.send(users);
});Fastify Schema Validation
Defining Schemas
Schemas are JSON objects using JSON Schema to define request or response structures:
const schema = {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' }
}
},
querystring: {
type: 'object',
properties: {
page: { type: 'integer', minimum: 1 },
limit: { type: 'integer', minimum: 1, maximum: 100 }
}
}
};Applying Schemas
Schemas are applied in route options for automatic validation:
const fastify = require('fastify')();
const schema = {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' }
}
}
};
fastify.post('/users', { schema }, async (request, reply) => {
const { name, email } = request.body;
return { name, email };
});Error Handling
Invalid data triggers a 400 Bad Request error. Customize error handling:
fastify.setErrorHandler((error, request, reply) => {
if (error.validation) {
reply.status(422).send({
error: 'Unprocessable Entity',
message: error.validation.map(err => err.message)
});
} else {
reply.status(500).send({ error: 'Internal Server Error' });
}
});Validating Responses
Validate response data for consistency:
const schema = {
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' }
}
}
}
};
fastify.get('/users/:id', { schema }, async (request, reply) => {
const user = { name: 'John Doe', email: 'john.doe@example.com' };
return user;
});Advanced Schema Features
Custom Ajv Validator
Fastify uses Ajv for validation, allowing custom keywords:
const Ajv = require('ajv');
const fastify = require('fastify')();
const schema = {
body: {
type: 'object',
properties: {
password: { type: 'string', minLength: 8, isStrongPassword: true }
}
}
};
const ajv = new Ajv();
ajv.addKeyword({
keyword: 'isStrongPassword',
validate: (schema, data) => {
return /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/.test(data);
}
});
fastify.setValidatorCompiler(() => ajv);
fastify.post('/register', { schema }, async (request, reply) => {
return request.body;
});Asynchronous Schema Validation
Ajv supports async validation for external data checks:
const Ajv = require('ajv');
const fastify = require('fastify')();
const schema = {
body: {
type: 'object',
properties: {
username: { type: 'string', isUniqueUsername: true }
}
}
};
const ajv = new Ajv({ removeAdditional: true });
ajv.addKeyword({
keyword: 'isUniqueUsername',
async: true,
validate: async (schema, data) => {
const exists = await checkUsernameInDatabase(data); // Hypothetical
return !exists;
}
});
fastify.setValidatorCompiler(() => ajv);
fastify.post('/register', { schema }, async (request, reply) => {
return request.body;
});Multilingual Error Messages
Support i18n error messages:
const Ajv = require('ajv');
const fastify = require('fastify')();
const schema = {
body: {
type: 'object',
properties: {
name: {
type: 'string',
minLength: 3,
errorMessage: {
minLength: {
en: 'Name must be at least 3 characters long.',
es: 'El nombre debe tener al menos 3 caracteres.'
}
}
}
}
}
};
const ajv = new Ajv({ messages: false });
fastify.setValidatorCompiler(() => ajv);
fastify.post('/users', { schema }, async (request, reply) => {
return request.body;
});Decorators and Preprocessors
Decorators
Decorators extend Fastify instances, requests, or replies:
- Fastify Instance: Use
fastify.decorate(). - Request: Use
fastify.decorateRequest(). - Reply: Use
fastify.decorateReply().
Example:
fastify.decorate('db', require('./database'));Access the database via fastify.db.
Preprocessors
Preprocessors run before route handlers, performing tasks like authentication:
const isLoggedIn = (request, reply, done) => {
if (!request.session.user) {
reply.code(401).send({ error: 'Unauthorized' });
} else {
done();
}
};
fastify.get('/protected', { preHandler: isLoggedIn }, (request, reply) => {
return { message: 'Protected route' };
});Advanced Decorator Usage
Dynamic Decorators
Generate methods dynamically:
fastify.decorate('logRequest', function (shouldLog) {
if (shouldLog) {
return function (request, reply, done) {
console.log('Request received:', request.url);
done();
};
}
return null;
});
fastify.get('/test', { preHandler: fastify.logRequest(true) }, (request, reply) => {
return { message: 'Test route' };
});Decorator Chains
Chain decorators for combined functionality:
fastify.decorate('auth', require('./middlewares/auth'));
fastify.decorate('log', require('./middlewares/log'));
fastify.get('/secure', { preHandler: [fastify.auth, fastify.log] }, (request, reply) => {
return { message: 'Secure route' };
});Advanced Preprocessor Usage
Error Handling
Throw errors in preprocessors to halt requests:
const checkUser = (request, reply, done) => {
if (!request.session.user) {
throw new Error('Unauthorized');
}
done();
};
fastify.setErrorHandler((error, request, reply) => {
reply.status(401).send({ error: 'Unauthorized' });
});
fastify.get('/secure', { preHandler: checkUser }, (request, reply) => {
return { message: 'Secure route' };
});Async Preprocessors
Use async/await in preprocessors:
const checkUser = async (request, reply) => {
const user = await getUserFromDB(request.session.userId);
if (!user) {
throw new Error('Unauthorized');
}
};
fastify.get('/secure', { preHandler: checkUser }, (request, reply) => {
return { message: 'Secure route' };
});Decorators vs. Preprocessors
- Decorators: Extend object functionality without blocking requests.
- Preprocessors: Run before handlers, controlling request flow.
Combining Decorators and Preprocessors
fastify.decorate('preAuth', function (request, reply, done) {
if (!request.session.user) {
reply.code(401).send({ error: 'Unauthorized' });
} else {
done();
}
});
fastify.get('/protected', { preHandler: fastify.preAuth }, (request, reply) => {
return { message: 'Protected route' };
});Security
Authentication and Authorization
Authentication verifies user identity; authorization controls resource access.
Authentication Middleware
Use middleware for authentication, like JWT verification:
const jwt = require('jsonwebtoken');
const authenticate = (request, reply, done) => {
try {
const token = request.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, 'your-secret-key');
request.user = decoded;
done();
} catch (err) {
reply.code(401).send({ error: 'Unauthorized' });
}
};
fastify.get('/secure', { preHandler: authenticate }, (request, reply) => {
return { message: 'Authenticated route' };
});Security Plugins
Use plugins like fastify-jwt:
const fastifyJwt = require('fastify-jwt');
fastify.register(fastifyJwt, {
secret: 'your-secret-key',
sign: {
expiresIn: '1h'
}
});
fastify.post('/login', async (request, reply) => {
const { username, password } = request.body;
if (checkCredentials(username, password)) {
const token = await fastify.jwt.sign({ username });
reply.send({ token });
} else {
reply.code(401).send({ error: 'Invalid credentials' });
}
});Security Plugin Integration
Enhance security with plugins:
const fastifyHelmet = require('@fastify/helmet');
const fastifyCORS = require('@fastify/cors');
const fastifyRateLimit = require('@fastify/rate-limit');
fastify.register(fastifyHelmet);
fastify.register(fastifyCORS);
fastify.register(fastifyRateLimit, {
max: 100,
timeWindow: '1 minute'
});Input Validation and Sanitization
Use schema validation to prevent attacks:
const schema = {
body: {
type: 'object',
required: ['username', 'password'],
properties: {
username: { type: 'string', minLength: 3 },
password: { type: 'string', minLength: 8 }
}
}
};
fastify.post('/register', { schema }, (request, reply) => {
return request.body;
});HTTPS Encryption
Enable HTTPS with SSL/TLS:
const fs = require('fs');
const options = {
key: fs.readFileSync('path/to/private.key'),
cert: fs.readFileSync('path/to/certificate.crt')
};
const fastify = require('fastify')({ https: options });
fastify.listen({ port: 443 }, (err, address) => {
if (err) throw err;
console.log(`Server listening on ${address}`);
});Logging and Monitoring
Use custom loggers for security events:
const winston = require('winston');
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'access.log' })
]
});
const fastify = require('fastify')({ logger });
fastify.addHook('onRequest', (request, reply, done) => {
logger.info(`Received request to ${request.url}`);
done();
});
fastify.setErrorHandler((error, request, reply) => {
logger.error(`Error handling request to ${request.url}: ${error.message}`);
reply.status(500).send({ error: 'Internal Server Error' });
});Regular Updates and Security Patches
Check for updates with npm outdated and apply them with npm update:
npm outdated
npm updateCombining Middleware and Security Plugins
const fastifyCsrf = require('@fastify/csrf-protection');
const fastifyHelmet = require('@fastify/helmet');
fastify.register(fastifyCsrf);
fastify.register(fastifyHelmet);
const isAdmin = (request, reply, done) => {
if (!request.user || !request.user.isAdmin) {
reply.code(403).send({ error: 'Forbidden' });
} else {
done();
}
};
fastify.get('/admin', { preHandler: isAdmin }, (request, reply) => {
return { message: 'Admin route' };
});Hook Methods
Hook Overview
Hooks extend Fastify behavior, including instance, request, and decorator hooks.
Instance Hooks
Register Instance Hooks:
fastify.addHook('onReady', async (done) => {
console.log('Application is ready');
done();
});Example:
fastify.addHook('onClose', async (done) => {
console.log('Application is closing');
done();
});Request Hooks
Add Request Hooks:
fastify.addHook('onRequest', async (request, reply) => {
console.log('Handling request');
});Example:
fastify.addHook('preValidation', async (request, reply) => {
console.log('Before validation');
});Decorator Hooks
Decorate Fastify Instance:
fastify.decorate('myMethod', (callback) => {
console.log('Calling myMethod');
callback();
});Decorate Request Object:
fastify.decorateRequest('myRequestMethod', (callback) => {
console.log('Calling myRequestMethod');
callback();
});Combining Hooks
fastify.addHook('onReady', async (done) => {
console.log('Application is ready');
done();
});
fastify.addHook('onRequest', async (request, reply) => {
console.log('Handling request');
});Example Code
const fastify = require('fastify')({ logger: true });
fastify.addHook('onReady', async (done) => {
console.log('Application is ready');
done();
});
fastify.addHook('onRequest', async (request, reply) => {
console.log('Handling request');
});
fastify.addHook('preValidation', async (request, reply) => {
console.log('Before validation');
});
fastify.addHook('preHandler', async (request, reply) => {
console.log('Before handler');
});
fastify.addHook('onSend', async (request, reply, payload) => {
console.log('Before sending response');
return payload;
});
fastify.addHook('onResponse', async (request, reply) => {
console.log('After sending response');
});
fastify.addHook('onClose', async (done) => {
console.log('Application is closing');
done();
});
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
fastify.listen({ port: 3000 }, (err, address) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
console.log(`Server listening on ${address}`);
});Hook Execution Order
- onReady
- onRequest
- preValidation
- preHandler
- Route Handler
- onSend
- onResponse
- onClose
Error Handling
fastify.addHook('onRequest', async (request, reply) => {
if (request.url === '/error') {
throw new Error('Simulated error');
}
});
fastify.setErrorHandler((error, request, reply) => {
console.error(error);
reply.code(500).send({ error: 'Internal server error' });
});Testing Hooks
const { expect } = require('chai');
const fastify = require('fastify')({ logger: true });
describe('Hooks', () => {
it('should run onRequest hook', async () => {
fastify.addHook('onRequest', async (request, reply) => {});
const response = await fastify.inject({
method: 'GET',
url: '/'
});
expect(response.statusCode).to.equal(404); // No route defined
});
});Advanced Hook Usage
Dynamic Hook Addition:
fastify.addHook('onRequest', async (request, reply) => {
console.log('Handling request');
});
fastify.addHook('preHandler', async (request, reply) => {
console.log('Before handler');
});Removing Hooks:
let hookId;
fastify.addHook('onRequest', (request, reply, done) => {
console.log('Handling request');
done();
}, { name: 'myHook' }, (id) => {
hookId = id;
});
fastify.removeHook('onRequest', hookId);Parallel vs. Serial Hooks
Parallel Execution:
fastify.addHook('onRequest', async (request, reply) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Handling request 1');
});
fastify.addHook('onRequest', async (request, reply) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Handling request 2');
});Serial Execution:
fastify.addHook('onRequest', async (request, reply) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Handling request 1');
console.log('Handling request 2');
});Hook Parameter Passing
Passing Parameters:
fastify.addHook('onRequest', async (request, reply) => {
request.myData = 'some data';
});
fastify.get('/', async (request, reply) => {
console.log(request.myData); // some data
return { hello: 'world' };
});Returning Values:
fastify.addHook('preHandler', async (request, reply) => {
request.data = { data: 'from hook' };
});
fastify.get('/', async (request, reply) => {
console.log(request.data); // { data: 'from hook' }
return { hello: 'world' };
});Hooks with Async Operations
Async Hooks:
fastify.addHook('onRequest', async (request, reply) => {
await someAsyncOperation();
});Error Handling:
fastify.addHook('onRequest', async (request, reply) => {
try {
await someAsyncOperation();
} catch (error) {
throw error;
}
});Hook Concurrency Control
Limit Concurrency:
const { Semaphore } = require('async-mutex');
fastify.decorate('sema', new Semaphore(10));
fastify.addHook('onRequest', async (request, reply) => {
const release = await fastify.sema.acquire();
try {
console.log('Handling request');
} finally {
release();
}
});Hooks with Plugins
Register Hooks in Plugins:
fastify.register(async (instance) => {
instance.addHook('onRequest', async (request, reply) => {
console.log('Handling request');
});
});Hooks vs. Middleware
Middleware as Hooks:
fastify.use((request, reply, next) => {
console.log('Middleware running');
next();
});
fastify.addHook('onRequest', async (request, reply) => {
console.log('Handling request');
});Differences:
- Middleware: Focuses on request/response processing.
- Hooks: Inject logic at specific points.



