Skip to content
All essays
ArchitectureMarch 27, 202411 min

Building Fastify Plugins and Middleware

Create reusable plugins and middleware for your Fastify applications.

Ü
Ümit Uz
Mobile & Full Stack Developer

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}`);
});

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!

Enjoyed this essay? Share it.

Related essays

Next essay
Building Real-Time Applications with WebSockets