Lesson 54-Fastify Advanced Applications

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-mongodb

Load 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 update

Combining 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.
Share your love