Skip to content

Middleware Composition Guide

This guide explains how to use the middleware composition system to create reusable and composable middleware functions.

Overview

The middleware composition system provides:

  1. Token Verification: Base functions for verifying different types of tokens
  2. Permission Checking: Functions for checking user permissions
  3. Middleware Composition: Utilities for composing middleware functions
  4. Error Handling: Consistent error handling for middleware

Token Verification

The token verification system provides a base function for verifying different types of tokens:

JavaScript
const { verifyAccessToken, verifyOTPToken } = require('../app/handlers/middlewares');

// Use in route definition
fastify.get('/protected-route', {
  preHandler: verifyAccessToken
}, handler);

// Use in route definition with OTP token
fastify.post('/verify-otp', {
  preHandler: verifyOTPToken
}, handler);

Available Token Verifiers

  • verifyAccessToken: Verifies standard access tokens
  • verifyOTPToken: Verifies one-time password tokens
  • verifyTempPassToken: Verifies temporary password tokens
  • verifyRefreshToken: Verifies refresh tokens

Permission Checking

The permission checking system provides functions for checking user permissions:

JavaScript
const { checkPermission } = require('../app/handlers/middlewares');
const { PERMISSIONS } = require('../app/constants');

// Use in route definition
fastify.get('/admin/users', {
  preHandler: checkPermission(PERMISSIONS.VIEW_USERS)
}, handler);

// Check multiple permissions (ANY)
fastify.put('/admin/users/:id', {
  preHandler: checkPermission([
    PERMISSIONS.UPDATE_USERS,
    PERMISSIONS.MANAGE_USERS
  ])
}, handler);

// Check multiple permissions (ALL)
fastify.delete('/admin/users/:id', {
  preHandler: checkPermission([
    PERMISSIONS.DELETE_USERS,
    PERMISSIONS.MANAGE_USERS
  ], { requireAll: true })
}, handler);

Middleware Composition

The middleware composition system provides utilities for composing middleware functions:

JavaScript
const { 
  verifyAccessToken, 
  checkPermission,
  utils: { compose }
} = require('../app/handlers/middlewares');
const { PERMISSIONS } = require('../app/constants');

// Compose multiple middleware functions
const adminMiddleware = compose(
  verifyAccessToken,
  checkPermission(PERMISSIONS.ADMIN)
);

// Use in route definition
fastify.get('/admin/dashboard', {
  preHandler: adminMiddleware
}, handler);

Built-in Composed Middleware

  • authenticateAndAuthorize: Combines authentication and authorization
JavaScript
1
2
3
4
5
6
7
const { authenticateAndAuthorize } = require('../app/handlers/middlewares');
const { PERMISSIONS } = require('../app/constants');

// Use in route definition
fastify.get('/admin/users', {
  preHandler: authenticateAndAuthorize(PERMISSIONS.VIEW_USERS)
}, handler);

Creating Custom Middleware

You can create custom middleware using the utility functions:

JavaScript
const { 
  utils: { 
    createTokenVerifier, 
    createPermissionChecker,
    compose,
    withLogging,
    withErrorHandler,
    conditional
  }
} = require('../app/handlers/middlewares');

// Create a custom token verifier
const verifyApiKeyToken = createTokenVerifier('API_KEY', {
  errorMessage: 'Invalid API key',
  userFetcher: async (userRepository, apiKey) => {
    return await userRepository.findByApiKey(apiKey);
  }
});

// Create a conditional middleware
const skipInDevelopment = conditional(
  () => process.env.NODE_ENV !== 'development',
  checkPermission(PERMISSIONS.ADMIN)
);

// Create a middleware with logging
const loggingMiddleware = withLogging(
  async function myMiddleware(request, reply) {
    // Middleware logic
  },
  { name: 'myMiddleware' }
);

// Create a middleware with error handling
const errorHandlingMiddleware = withErrorHandler(
  async function riskyMiddleware(request, reply) {
    // Middleware that might throw errors
  },
  async function errorHandler(error, request, reply) {
    // Custom error handling
  }
);

// Compose all middleware
const customMiddleware = compose(
  verifyApiKeyToken,
  skipInDevelopment,
  loggingMiddleware,
  errorHandlingMiddleware
);

Middleware Utilities

Token Verifier Utilities

  • createTokenVerifier(tokenType, options): Creates a token verification middleware
  • verifyToken(request, options): Verifies a JWT token
  • validateTokenPayload(payload, tokenType, errorMessage): Validates token payload
  • fetchUserFromToken(userRepository, userId, options): Fetches user from database

Middleware Composer Utilities

  • compose(...middlewares): Composes multiple middleware functions
  • conditional(condition, middleware): Creates a conditional middleware
  • withErrorHandler(middleware, errorHandler): Adds error handling to middleware
  • withLogging(middleware, options): Adds logging to middleware
  • withCache(middleware, cacheKeyGenerator, options): Adds caching to middleware

Permission Checker Utilities

  • createPermissionChecker(requiredPermissions, options): Creates a permission checker middleware
  • hasPermission(user, requiredPermissions, requireAll): Checks if user has permissions

Best Practices

  1. Use Composition: Compose middleware from smaller, reusable functions
  2. Add Logging: Use withLogging to add logging to your middleware
  3. Handle Errors: Use withErrorHandler to handle errors consistently
  4. Be Conditional: Use conditional to skip middleware based on conditions
  5. Keep It Simple: Each middleware should do one thing well
  6. Reuse: Create reusable middleware functions for common tasks
  7. Test: Write tests for your middleware functions

Migration from Legacy Middleware

The legacy middleware functions are still available for backward compatibility:

  • authenticateverifyAccessToken
  • verifyOTPTokenverifyOTPToken (same name, new implementation)
  • verifyPassTokenverifyTempPassToken
  • verifyRefreshTokenverifyRefreshToken (same name, new implementation)
  • authorizecheckPermission

To migrate to the new system:

JavaScript
1
2
3
4
5
6
7
8
9
// Legacy
fastify.get('/protected', {
  preHandler: [authenticate, authorize(PERMISSIONS.VIEW_USERS)]
}, handler);

// New
fastify.get('/protected', {
  preHandler: authenticateAndAuthorize(PERMISSIONS.VIEW_USERS)
}, handler);

Examples

Basic Authentication

JavaScript
1
2
3
4
5
const { verifyAccessToken } = require('../app/handlers/middlewares');

fastify.get('/profile', {
  preHandler: verifyAccessToken
}, handler);

Role-Based Access Control

JavaScript
const { authenticateAndAuthorize } = require('../app/handlers/middlewares');
const { PERMISSIONS } = require('../app/constants');

fastify.get('/admin/users', {
  preHandler: authenticateAndAuthorize(PERMISSIONS.VIEW_USERS)
}, handler);

fastify.post('/admin/users', {
  preHandler: authenticateAndAuthorize(PERMISSIONS.CREATE_USERS)
}, handler);

fastify.put('/admin/users/:id', {
  preHandler: authenticateAndAuthorize(PERMISSIONS.UPDATE_USERS)
}, handler);

fastify.delete('/admin/users/:id', {
  preHandler: authenticateAndAuthorize(PERMISSIONS.DELETE_USERS)
}, handler);

Complex Workflow

JavaScript
const { 
  verifyAccessToken, 
  checkPermission,
  utils: { compose, conditional, withLogging }
} = require('../app/handlers/middlewares');
const { PERMISSIONS } = require('../app/constants');

// Check if user is an admin
const isAdmin = async function(request) {
  return request.userFromToken?.role?.name === 'admin';
};

// Skip permission check for admins
const checkPermissionUnlessAdmin = conditional(
  async function(request) {
    return !(await isAdmin(request));
  },
  checkPermission(PERMISSIONS.VIEW_SENSITIVE_DATA)
);

// Log access to sensitive data
const logSensitiveAccess = withLogging(
  async function(request, reply) {
    const { userFromToken } = request;
    this.diScope.resolve('logger').info('Sensitive data accessed', {
      userId: userFromToken.id,
      username: userFromToken.username,
      role: userFromToken.role?.name
    });
  },
  { name: 'logSensitiveAccess' }
);

// Compose middleware
const sensitiveDataMiddleware = compose(
  verifyAccessToken,
  checkPermissionUnlessAdmin,
  logSensitiveAccess
);

// Use in route
fastify.get('/sensitive-data', {
  preHandler: sensitiveDataMiddleware
}, handler);

Conclusion

The middleware composition system provides a flexible and reusable way to create middleware functions. By composing middleware from smaller, reusable functions, you can create complex workflows while keeping your code clean and maintainable.