Building a Modular Fastify Plugin with Encapsulated Decorators and Schema Validation
Step‑by‑step guide to create a Fastify plugin that adds a scoped decorator, validates JSON schemas, logs with Pino, and verifies encapsulation.
01 Jan 2026, 17:35 UTC

Desired outcome
Create a reusable Fastify plugin that adds a decorated utility (e.g., an authentication helper) to the request object, registers a route with JSON‑schema validation, logs requests via Pino, and keeps the decorator scoped to the plugin so it does not leak to other plugins.
Prerequisites
- Node.js ≥18 installed
- Fastify v4.x installed via npm (or yarn/pnpm)
- Basic familiarity with async/await, JSON Schema, and optionally TypeScript
- Access to a terminal with permission to run
nodeand install packages
Procedure
- Initialize a Fastify instance
// server.js import Fastify from 'fastify'; const fastify = Fastify({ logger: true }); - Define the plugin function
The plugin uses
fastify.decorateto attach a utility to the request object and registers a route that validates incoming JSON with a schema.// authPlugin.js import fp from 'fastify-plugin'; export default fp(async function (fastify, opts) { // 1. Decorate the request with a simple auth helper fastify.decorateRequest('auth', () => { // placeholder: real implementation would check headers/tokens return { isAuthenticated: true }; }); // 2. Register a route inside the plugin scope fastify.route({ method: 'POST', url: '/protected', schema: { body: { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] } }, handler: async (request, reply) => { // 3. Use the decorator inside the handler const auth = request.auth(); if (!auth.isAuthenticated) { return reply.code(401).send({ error: 'Unauthorized' }); } // Echo back the validated message return { received: request.body.message }; } }); }); - Register the plugin with encapsulation
// server.js (continued) import authPlugin from './authPlugin.js'; fastify.register(authPlugin); // plugin is encapsulated by default // Start the server const start = async () => { try { await fastify.listen({ port: 3000, host: '0.0.0.0' }); fastify.log.info(`Server listening on ${fastify.server.address().port}`); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); - Run the server
From the project root, execute:
node server.jsNo special privileges are required; the process binds to port 3000.
Expected checks
- Send a valid JSON payload (
{ "message": "hello" }) toPOST http://localhost:3000/protected; expect a 200 response with{ "received": "hello" }and a log line from Pino. - Send an invalid payload (e.g., missing
messageor wrong type); expect a 400 response with Fastify’s default validation error format. - From a route defined the plugin (e.g., a separate plugin or the root instance), attempt to access
request.auth; it should beundefined, confirming encapsulation. - Throw an error inside the handler (e.g.,
throw new Error('test')); verify that Fastify’s error handler logs the error and returns a 500 response with an error object.
Recovery options (rollback)
Registering a plugin mutates the Fastify instance’s internal state. To revert:
- Call
fastify.close()to shut down the server and release the port. - If you need to replace the plugin without restarting the whole process, first close the instance, then create a new Fastify instance and register the updated plugin.
- During development, tools like
fastify-cliwith the--watchflag can automate this close‑and‑re‑register cycle.
Limitations and practical verification
Plugin encapsulation works only when the plugin is registered via fastify.register (or fastify-plugin). If you accidentally call fastify.decorate on the top‑level instance, the decorator becomes globally available, breaking encapsulation. To verify that encapsulation is intact:
- Start the server as described.
- In a separate terminal, run a Node REPL and require the Fastify instance (if exposed) or simply attempt to access
request.authin a route outside the plugin; you should receiveundefined. - If the property is defined, review your code for any top‑level
fastify.decoratecalls and move them inside the plugin scope.
Remember that changing a route’s JSON schema after registration requires re‑registering the plugin (or creating a new Fastify instance); hot‑reloading without closing the instance may leave stale validation rules.
Diagram
- Fastify instance
- Plugin encapsulation boundary
- Request decorator (auth)
- Route with JSON schema
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.