Fastify's plugin system enables code reuse and modularity. Learn to create powerful plugins and middleware.
Understanding Plugins
What Are Plugins?
Plugins encapsulate functionality that can be shared across routes and applications.
javascript
const fp = require('fastify-plugin');
async function myPlugin(fastify, options) {
// Plugin logic
}
module.exports = fp(myPlugin, {
name: 'myPlugin',
});Creating Plugins
Basic Plugin Structure
javascript
// plugins/logger.js
const fp = require('fastify-plugin');
async function loggerPlugin(fastify, options) {
fastify.decorate('customLogger', {
info(message) {
console.log(`[INFO] ${message}`);
},
error(message) {
console.error(`[ERROR] ${message}`);
},
});
}
module.exports = fp(loggerPlugin, {
name: 'customLogger',
});Plugin with Dependencies
javascript
// plugins/db.js
const fp = require('fastify-plugin');
async function dbPlugin(fastify, options) {
const { url } = options;
// Check if required plugin is loaded
if (!fastify.mongodb) {
throw new Error('MongoDB plugin is required');
}
const client = new MongoClient(url);
await client.connect();
fastify.decorate('db', client.db());
// Cleanup on close
fastify.addHook('onClose', async (instance) => {
await client.close();
});
}
module.exports = fp(dbPlugin, {
name: 'db',
dependencies: ['fastify-mongodb'],
});Plugin Decorators
Adding Instance Methods
javascript
// plugins/user-service.js
const fp = require('fastify-plugin');
async function userServicePlugin(fastify) {
fastify.decorate('userService', {
async findById(id) {
return await fastify.db.users.findOne({ _id: id });
},
async create(data) {
return await fastify.db.users.insertOne(data);
},
async update(id, data) {
return await fastify.db.users.updateOne(
{ _id: id },
{ $set: data }
);
},
});
}
module.exports = fp(userServicePlugin);Using Decorators in Routes
javascript
// After registering plugin
fastify.register(require('./plugins/user-service'));
fastify.get('/user/:id', async (request, reply) => {
const user = await fastify.userService.findById(request.params.id);
return user;
});Request Decorators
Adding Request Properties
javascript
// plugins/auth.js
const fp = require('fastify-plugin');
async function authPlugin(fastify, options) {
fastify.decorateRequest('user', null);
fastify.addHook('onRequest', async (request, reply) => {
const token = request.headers.authorization;
if (token) {
try {
const decoded = fastify.jwt.verify(token);
request.user = decoded;
} catch (err) {
// Invalid token
}
}
});
}
module.exports = fp(authPlugin);Using Request Decorators
javascript
fastify.register(require('./plugins/auth'));
fastify.get('/profile', async (request, reply) => {
if (!request.user) {
return reply.code(401).send({ error: 'Unauthorized' });
}
return { user: request.user };
});Reply Decorators
Custom Response Methods
javascript
// plugins/response.js
const fp = require('fastify-plugin');
async function responsePlugin(fastify) {
fastify.decorateReply('success', function(data) {
return this.code(200).send({
success: true,
data,
});
});
fastify.decorateReply('error', function(message, code = 500) {
return this.code(code).send({
success: false,
error: message,
});
});
}
module.exports = fp(responsePlugin);Using Reply Decorators
javascript
fastify.register(require('./plugins/response'));
fastify.get('/data', async (request, reply) => {
const data = await fetchData();
return reply.success(data);
});
fastify.get('/error', async (request, reply) => {
return reply.error('Something went wrong', 500);
});Middleware
onRequest Hook
javascript
fastify.addHook('onRequest', async (request, reply) => {
console.log(`${request.method} ${request.url}`);
// Add custom header
reply.header('X-Response-Time', Date.now());
});preParsing Hook
javascript
fastify.addHook('preParsing', async (request, reply, payload) => {
console.log('Before parsing body');
return payload;
});preValidation Hook
javascript
fastify.addHook('preValidation', async (request, reply) => {
// After body parsing, before validation
const authHeader = request.headers.authorization;
if (!authHeader) {
return reply.code(401).send({ error: 'No auth header' });
}
});preHandler Hook
javascript
fastify.addHook('preHandler', async (request, reply) => {
// Before route handler, after validation
console.log('Running preHandler');
});onResponse Hook
javascript
fastify.addHook('onResponse', async (request, reply) => {
console.log(`Response sent with status ${reply.statusCode}`);
});Popular Plugins
CORS
javascript
const fastify = require('fastify')();
fastify.register(require('@fastify/cors'), {
origin: true,
credentials: true,
});Helmet
javascript
fastify.register(require('@fastify/helmet'), {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
});Rate Limiting
javascript
fastify.register(require('@fastify/rate-limit'), {
max: 100,
timeWindow: '1 minute',
});
fastify.get('/limited', async (request, reply) => {
return { message: 'This endpoint is rate limited' };
});Swagger/OpenAPI
javascript
fastify.register(require('@fastify/swagger'), {
exposeRoute: true,
swagger: {
info: {
title: 'API Documentation',
description: 'API documentation',
version: '1.0.0',
},
},
});
fastify.register(require('@fastify/swagger-ui'), {
routePrefix: '/docs',
});
// Define route with schema
fastify.get('/users', {
schema: {
description: 'Get all users',
tags: ['users'],
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
},
},
},
}, async (request, reply) => {
return await getUsers();
});Error Handling Plugins
Global Error Handler
javascript
// plugins/error-handler.js
const fp = require('fastify-plugin');
async function errorHandlerPlugin(fastify) {
fastify.setErrorHandler((error, request, reply) => {
fastify.log.error(error);
if (error.validation) {
reply.status(400).send({
success: false,
error: 'Validation Error',
details: error.validation,
});
return;
}
reply.status(error.statusCode || 500).send({
success: false,
error: error.message || 'Internal Server Error',
});
});
}
module.exports = fp(errorHandlerPlugin);Custom Error Classes
javascript
// errors/AppError.js
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.name = 'AppError';
}
}
// errors/NotFoundError.js
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
this.name = 'NotFoundError';
}
}
// Usage in routes
fastify.get('/user/:id', async (request, reply) => {
const user = await findUser(request.params.id);
if (!user) {
throw new NotFoundError('User not found');
}
return user;
});Testing Plugins
Unit Testing
javascript
const { test } = require('tap');
const build = require('../app');
test('plugin loads successfully', async (t) => {
const app = build();
await app.ready();
t.ok(app.customLogger, 'Logger plugin loaded');
await app.close();
});Best Practices
DOs
- Use fp() for plugins
- Decorate with specific functionality
- Handle cleanup in onClose
- Document plugin options
- Use TypeScript for type safety
- Test plugins independently
- Handle errors gracefully
DON'Ts
- Overuse decorators
- Skip error handling
- Create circular dependencies
- Ignore plugin lifecycle
- Mix concerns
- Forget about cleanup
- Skip documentation
Conclusion
Fastify plugins enable code reuse and modularity. Create focused plugins, use decorators wisely, and handle the plugin lifecycle properly. The plugin ecosystem is Fastify's strength!