| |

Node.js 36 ๐ŸŸข Fastify Plugins, Hooks, Lifecycle, and Schema Validation

Fastify is built on a single architectural bet: encapsulation. Every call to register() creates a new child context, and anything a plugin adds to itself โ€” routes, decorators, hooks โ€” is visible to that plugin’s descendants but invisible to its siblings and ancestors . This is not a minor implementation detail. It is the mechanism that prevents a third-party plugin from reaching into another plugin’s database connection, and it is the reason a malformed auth hook registered at the wrong scope cannot silently apply to routes it was never meant to guard .

Plugins, hooks, the request lifecycle, and schema validation are the four pillars of a Fastify application. Plugins are the unit of encapsulation. Hooks are the injection points into the request lifecycle. The lifecycle is the ordered sequence of steps a request passes through. Schema validation is the first line of defense, rejecting invalid input before it reaches a handler. Understanding how these four interact is the difference between a Fastify application that is fast and a Fastify application that is correct.

This chapter covers three areas. First, why plugins and encapsulation exist โ€” the shared-mutable-state problem, the context isolation model, and the fastify-plugin escape hatch. Second, how the request lifecycle and hooks work โ€” the ordered phases, the difference between onRequest and preHandler, and the scoping rules that determine where a hook runs. Third, how schema validation works โ€” the body, querystring, params, and headers validators, the Ajv engine, and the validation error handling contract. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the plugin tree and the lifecycle.

Key point: Fastify plugins create encapsulated contexts. A plugin’s decorators and hooks are visible to its descendants but not to its siblings or ancestors. fastify-plugin breaks encapsulation deliberately, making a plugin’s additions visible to the parent scope. Hooks are scoped to the plugin where they are registered. Schema validation happens before the handler, with body, query, params, and headers validated by Ajv.


Why plugins and encapsulation exist

The shared-mutable-state problem. In a framework with a single global application object, every plugin sees and can modify the same state. A plugin that registers an authentication hook at the top level affects every route in the application, including routes registered before and after it. A plugin that decorates the application with a utility function exposes that function everywhere. This makes the system flexible but fragile: a change in one plugin can break an unrelated plugin, and the order of registration becomes a hidden dependency. Fastify’s encapsulation model replaces the global object with a tree of contexts. Each register() call creates a child context, and a plugin’s additions live in that context. The plugin tree becomes a directed acyclic graph of scopes, and the position of a plugin in the tree determines what it can see and what can see it .

The sibling-isolation problem. The encapsulation rule is precise: a plugin’s decorators are visible to its descendants but not to its siblings or ancestors . If plugin A registers a db decorator, and plugin B is registered as a sibling of A, plugin B cannot access db. If plugin C is registered as a child of A, plugin C can access db. This means two plugins can use the same decorator name without conflict, and a plugin cannot accidentally depend on a sibling’s state. The isolation is enforced by the context object, which is a clone of the parent instance with its own prototype chain .

The deliberate-break problem. Some plugins exist specifically to add cross-cutting functionality โ€” a database pool, a logger, an authentication decorator โ€” that every route needs. These plugins must break encapsulation so their additions are visible to the entire application. The fastify-plugin package does exactly this: it wraps a plugin function and flags it so Fastify skips creating a new child context. The plugin’s decorators and hooks attach to the parent scope, making them available to all descendants and siblings . The fastify-plugin wrapper also declares a semver range, a plugin name, and dependencies, which Fastify checks at boot and fails fast on if a version mismatch exists . The discipline is narrow scope: a plugin wrapped in fastify-plugin should decorate and nothing else. The moment it also registers routes or side-effect hooks, those live at the top of the tree, visible to and potentially interfering with everything registered afterward .

The skip-override problem. Fastify exposes a hidden Symbol.for('skip-override') property that achieves the same effect as fastify-plugin without any of its guardrails โ€” no semver check, no declared name, no dependency validation, and often no visible marker in a code review . Fastify’s own documentation labels this usage “not recommended” because it is a footgun path meant for framework-level tooling, not application code. A reviewer scanning a diff for fastify-plugin imports will not catch a plugin that sets this symbol directly, because it reads like an ordinary property assignment .

The trade-off. Encapsulation makes plugins isolated and safe, but it also means that a plugin cannot share state with a sibling without an explicit mechanism. The fastify-plugin wrapper is the mechanism, but it trades isolation for global visibility. The choice is between the safety of a scoped context and the convenience of a shared decorator. The rule is to use fastify-plugin only for genuinely cross-cutting concerns, and to keep the plugin’s surface area minimal.


a. Plugins and the encapsulation tree

A Fastify plugin is an async function that receives the Fastify instance and an options object. It is registered with fastify.register(plugin, options) .

const fp = require('fastify-plugin');

async function databasePlugin(fastify, options) {
  const pool = new Pool({ connectionString: options.url });
  fastify.decorate('db', pool);

  fastify.addHook('onClose', async () => {
    await pool.end();
  });
}

module.exports = fp(databasePlugin, {
  name: 'database-plugin',
});

Without the fp wrapper, the plugin’s db decorator is visible only to the plugin’s descendants. With the wrapper, it is visible to the entire application. The wrapper also sets name and dependencies metadata that Fastify uses for validation .

The plugin tree is built by registration order. Plugins registered at the top level are siblings of each other. Plugins registered inside another plugin are children of that plugin. The encapsulation rule means that a child can see the parent’s decorators, but a sibling cannot see another sibling’s decorators .

const app = Fastify();

// Top-level plugin: visible to all descendants
app.register(async function configPlugin(fastify) {
  fastify.decorate('config', { port: 3000 });

  // Child plugin: can see config
  fastify.register(async function routesPlugin(fastify) {
    fastify.get('/', async function () {
      return { port: this.config.port }; // works
    });
  });
});

// Sibling plugin: cannot see config
app.register(async function otherPlugin(fastify) {
  fastify.get('/other', async function () {
    return { port: this.config.port }; // undefined
  });
});

The encapsulation applies to ancestors and siblings, but not to children . A child inherits the parent’s context, so the parent’s decorators are available. A sibling does not inherit the other sibling’s context, so their decorators are not available.

b. The request lifecycle and hooks

Fastify processes each request through an ordered sequence of phases, called the lifecycle. Hooks are functions that a plugin can register to inject logic into a specific phase .

The lifecycle phases in order :

  1. onRequest โ€” The first hook. Runs before any parsing. Used for request ID generation, timing, and logging .
  2. preParsing โ€” Runs before the body is parsed. Used for custom content-type handling.
  3. preValidation โ€” Runs before validation. Used for transforming the raw request.
  4. preHandler โ€” Runs after validation, before the handler. Used for authentication and authorization .
  5. Handler โ€” The route’s handler function.
  6. preSerialization โ€” Runs before the response is serialized. Used for transforming the payload.
  7. onSend โ€” Runs before the response is sent. Used for adding headers.
  8. onResponse โ€” Runs after the response is sent. Used for metrics and logging .
  9. onError โ€” Runs when an error occurs.
  10. onTimeout โ€” Runs when a request times out.

Hooks are registered with fastify.addHook(name, fn). The hook is scoped to the plugin where it is registered. A hook registered in a child plugin runs only for routes in that plugin and its descendants .

fastify.register(async function protectedRoutes(fastify) {
  fastify.addHook('preHandler', async (request, reply) => {
    const token = request.headers.authorization;
    if (!token) {
      return reply.status(401).send({ error: 'Unauthorized' });
    }
  });

  fastify.get('/admin', async () => ({ admin: true }));
});

// The preHandler hook does not run for routes outside this plugin.

Route-level hooks can be declared in the route options. These hooks are unique to the route and always run as the last hook in their category .

fastify.get('/profile', {
  preHandler: async (request, reply) => {
    // runs after plugin-level preHandler hooks
  },
}, async (request) => {
  return { user: request.user };
});

The this context in a hook is the Fastify instance of the plugin where the hook was registered. This means a hook can access the plugin’s decorators through this .

c. Schema validation

Fastify uses Ajv v8 for JSON Schema validation. The validation is configured in the route’s schema object. The supported validators are body, querystring (or query), params, and headers .

fastify.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string', minLength: 1 },
        email: { type: 'string', format: 'email' },
        age: { type: 'integer', minimum: 0 },
      },
    },
    response: {
      201: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          name: { type: 'string' },
        },
      },
    },
  },
}, async (request, reply) => {
  const { name, email, age } = request.body; // validated and typed
  return reply.code(201).send({ id: '123', name });
});

The schema can be a complete JSON Schema object with type: 'object' and properties, or a simpler variation with the properties at the top level .

Ajv coerces values to the types specified in the schema. A query string parameter of "5" becomes the number 5 if the schema says type: 'integer'. This coercion is enabled by default but can be disabled with a custom validator compiler .

Validation errors are returned with a 400 status code by default. The error object contains validation and validationContext properties that can be used in a custom error handler .

fastify.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    return reply.status(400).send({
      message: `Validation failed for ${error.validationContext}`,
      errors: error.validation,
    });
  }
  reply.send(error);
});

The response schema is used for serialization, not validation. Fastify uses fast-json-stringify to serialize the response according to the schema, which increases throughput and prevents accidental disclosure of sensitive fields . If a field is not in the response schema, it is not serialized.


Complete Example Session

// ============================================
// PART 1: A BASIC PLUGIN WITH ENCAPSULATION
// ============================================

const Fastify = require('fastify');
const app = Fastify({ logger: true });

app.register(async function userPlugin(fastify, options) {
  // This decorator is visible only within this plugin
  // and its descendants.
  fastify.decorate('userService', {
    findById: async (id) => ({ id, name: 'Alice' }),
  });

  fastify.get('/users/:id', async function (request) {
    return this.userService.findById(request.params.id);
  });
});

app.get('/public', async function () {
  // this.userService is undefined here
  return { status: 'ok' };
});


// ============================================
// PART 2: BREAKING ENCAPSULATION WITH fastify-plugin
// ============================================

const fp = require('fastify-plugin');

const databasePlugin = fp(async function (fastify, options) {
  fastify.decorate('db', { query: async () => [] });

  fastify.addHook('onClose', async () => {
    fastify.log.info('Database connection closed');
  });
}, { name: 'database' });

app.register(databasePlugin);

// Now this.db is available everywhere
app.get('/data', async function () {
  return this.db.query();
});


// ============================================
// PART 3: THE onRequest HOOK
// ============================================

app.register(async function requestContext(fastify) {
  fastify.addHook('onRequest', async (request) => {
    request.requestId = request.headers['x-request-id'] || crypto.randomUUID();
    request.startTime = process.hrtime.bigint();
    request.log.info({ requestId: request.requestId }, 'Request started');
  });

  fastify.get('/tracked', async (request) => {
    return { requestId: request.requestId };
  });
});


// ============================================
// PART 4: THE preHandler HOOK FOR AUTH
// ============================================

app.register(async function protectedRoutes(fastify) {
  fastify.decorateRequest('userId', null);

  fastify.addHook('preHandler', async (request, reply) => {
    const auth = request.headers.authorization;
    if (!auth || !auth.startsWith('Bearer ')) {
      return reply.status(401).send({ error: 'Unauthorized' });
    }
    request.userId = auth.slice(7);
  });

  fastify.get('/profile', async (request) => {
    return { userId: request.userId };
  });
});


// ============================================
// PART 5: THE onResponse HOOK FOR METRICS
// ============================================

app.register(async function metrics(fastify) {
  fastify.addHook('onResponse', async (request, reply) => {
    const duration = Number(process.hrtime.bigint() - request.startTime) / 1e6;
    request.log.info({
      requestId: request.requestId,
      method: request.method,
      url: request.url,
      statusCode: reply.statusCode,
      durationMs: duration.toFixed(2),
    }, 'Request completed');
  });
});


// ============================================
// PART 6: SCHEMA VALIDATION FOR BODY
// ============================================

app.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string', minLength: 1, maxLength: 100 },
        email: { type: 'string', format: 'email' },
        age: { type: 'integer', minimum: 0, maximum: 150 },
      },
      additionalProperties: false,
    },
  },
}, async (request, reply) => {
  const { name, email, age } = request.body;
  return reply.code(201).send({ id: 'user-1', name, email, age });
});


// ============================================
// PART 7: SCHEMA VALIDATION FOR QUERY AND PARAMS
// ============================================

app.get('/search', {
  schema: {
    querystring: {
      type: 'object',
      required: ['q'],
      properties: {
        q: { type: 'string', minLength: 1 },
        limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
        offset: { type: 'integer', minimum: 0, default: 0 },
      },
    },
  },
}, async (request) => {
  return { query: request.query.q, limit: request.query.limit };
});

app.get('/items/:id', {
  schema: {
    params: {
      type: 'object',
      properties: {
        id: { type: 'string', pattern: '^[0-9]+$' },
      },
    },
  },
}, async (request) => {
  return { id: request.params.id };
});


// ============================================
// PART 8: RESPONSE SCHEMA FOR SERIALIZATION
// ============================================

app.get('/users/:id', {
  schema: {
    params: {
      type: 'object',
      properties: { id: { type: 'string' } },
    },
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          name: { type: 'string' },
          // email is not in the schema, so it is not serialized
        },
      },
    },
  },
}, async (request) => {
  return {
    id: request.params.id,
    name: 'Alice',
    email: 'alice@example.com', // stripped by serialization
  };
});


// ============================================
// PART 9: CUSTOM VALIDATION ERROR HANDLER
// ============================================

app.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    return reply.status(400).send({
      statusCode: 400,
      error: 'Bad Request',
      message: `Validation failed for ${error.validationContext}`,
      details: error.validation,
    });
  }

  request.log.error(error);
  reply.status(error.statusCode || 500).send({
    statusCode: error.statusCode || 500,
    error: 'Internal Server Error',
  });
});


// ============================================
// PART 10: THE COMPLETE APPLICATION
// ============================================

async function buildApp() {
  const app = Fastify({ logger: true });

  // Shared plugins (break encapsulation)
  await app.register(require('./plugins/config'));
  await app.register(require('./plugins/database'));

  // Encapsulated feature plugins
  await app.register(require('./plugins/users'), { prefix: '/api/users' });
  await app.register(require('./plugins/auth'), { prefix: '/api/auth' });

  // Global error handler
  app.setErrorHandler(require('./errors/handler'));

  return app;
}

buildApp().then((app) => {
  app.listen({ port: 3000 });
});

The ten parts show a basic encapsulated plugin, breaking encapsulation with fastify-plugin, the onRequest hook, the preHandler hook for auth, the onResponse hook for metrics, body validation, query and params validation, response schema serialization, a custom error handler, and the complete application structure.


Quick Reference

Plugin Registration

APIPurpose
fastify.register(plugin, options)Register a plugin in a child context
fastify-plugin wrapperBreak encapsulation; share with parent
fastify.decorate(name, value)Add a property to the instance
fastify.decorateRequest(name, value)Add a property to the request
plugin.after(callback)Run after a plugin is loaded

Lifecycle Hooks

HookPhaseUse case
onRequestFirstRequest ID, timing, logging
preParsingBefore body parseCustom content types
preValidationBefore validationTransform raw request
preHandlerAfter validationAuthentication, authorization
preSerializationBefore serializeTransform payload
onSendBefore sendAdd headers
onResponseAfter responseMetrics, logging
onErrorOn errorError handling
onTimeoutOn timeoutCleanup

Schema Validators

KeyValidates
bodyPOST/PUT/PATCH body
querystring or queryQuery string
paramsRoute parameters
headersRequest headers
responseResponse serialization (not validation)

Encapsulation Rules

RuleBehavior
Child sees parentโœ… Parent decorators available
Parent sees childโŒ Child decorators not available
Sibling sees siblingโŒ Isolated
fastify-pluginBreaks encapsulation; visible to parent

Best Practices

โœ… Do This:

// Use fastify-plugin for cross-cutting concerns
const fp = require('fastify-plugin');
module.exports = fp(async (fastify) => { ... }, { name: 'db' });         // โœ…
// Register hooks at the plugin level for scoped protection
fastify.addHook('preHandler', auth);                                     // โœ…
// Use schema validation for all input
schema: { body: { type: 'object', required: ['name'] } }                 // โœ…
// Use response schema to prevent data leaks
schema: { response: { 200: { type: 'object', properties: { id: {} } } } }// โœ…
// Validate plugin options
if (typeof options.ttl !== 'number') throw new Error('ttl required');    // โœ…
// Use route-level hooks for route-specific logic
fastify.get('/admin', { preHandler: [adminOnly] }, handler);             // โœ…

โŒ Don’t Do This:

// Don't use fastify-plugin for feature plugins
module.exports = fp(async (fastify) => { fastify.get('/route', ...) });  // โŒ
// Don't rely on sibling plugin decorators
// Plugin A's decorator is not visible to Plugin B                       // โŒ
// Don't skip schema validation
fastify.post('/users', async (request) => { ... }); // no schema         // โŒ
// Don't use skip-override symbol directly
module.exports[Symbol.for('skip-override')] = true;                      // โŒ
// Don't register global hooks for route-specific concerns
fastify.addHook('preHandler', adminAuth); // applies to all routes       // โŒ

Common Pitfalls

PitfallWhy It HappensFix
Decorator not available in siblingEncapsulation isolationUse fastify-plugin or restructure
Auth hook applies to wrong routesHook registered at wrong scopeRegister in the plugin that needs it
Validation not workingSchema missing or wrong keyCheck body/query/params keys
Response includes sensitive fieldsNo response schemaAdd response schema
Plugin order mattersAsync loadingUse .after() for sequential dependencies
this is undefined in arrow hooksArrow functions don’t bind thisUse regular functions
Custom content type not validatedNo content key in schemaAdd content-type mapping

Real-World Examples

1. Basic Plugin

app.register(async (fastify) => { fastify.get('/', async () => ({})); });

2. fastify-plugin Wrapper

const fp = require('fastify-plugin');
module.exports = fp(async (fastify) => { fastify.decorate('db', pool); });

3. onRequest Hook

fastify.addHook('onRequest', async (request) => { request.id = uuid(); });

4. preHandler Auth

fastify.addHook('preHandler', async (request, reply) => { /* auth */ });

5. onResponse Metrics

fastify.addHook('onResponse', async (request, reply) => { /* log */ });

6. Body Schema

schema: { body: { type: 'object', required: ['name'] } }

7. Query Schema

schema: { querystring: { type: 'object', properties: { q: {} } } }

8. Response Schema

schema: { response: { 200: { type: 'object', properties: { id: {} } } } }

9. Custom Error Handler

fastify.setErrorHandler((error, request, reply) => { /* handle */ });

10. Plugin with Options

app.register(cachePlugin, { ttl: 60000 });

Visual

The Plugin Encapsulation Tree

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PLUGIN ENCAPSULATION TREE                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  Root Instance                                               โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  app.decorate('global', ...)                         โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚         โ–ผ                      โ–ผ                      โ–ผ      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚
โ”‚  โ”‚ Plugin A     โ”‚      โ”‚ Plugin B     โ”‚      โ”‚ Plugin C     โ”‚โ”‚
โ”‚  โ”‚              โ”‚      โ”‚              โ”‚      โ”‚              โ”‚โ”‚
โ”‚  โ”‚ decorator A  โ”‚      โ”‚ decorator B  โ”‚      โ”‚              โ”‚โ”‚
โ”‚  โ”‚              โ”‚      โ”‚              โ”‚      โ”‚              โ”‚โ”‚
โ”‚  โ”‚ Sees: global โ”‚      โ”‚ Sees: global โ”‚      โ”‚ Sees: global โ”‚โ”‚
โ”‚  โ”‚ Does NOT see โ”‚      โ”‚ Does NOT see โ”‚      โ”‚              โ”‚โ”‚
โ”‚  โ”‚ decorator B  โ”‚      โ”‚ decorator A  โ”‚      โ”‚              โ”‚โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                            โ”‚
โ”‚  โ”‚ Plugin A1    โ”‚                                            โ”‚
โ”‚  โ”‚              โ”‚                                            โ”‚
โ”‚  โ”‚ Sees: global โ”‚                                            โ”‚
โ”‚  โ”‚ Sees: decorator A (inherited from parent)                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                            โ”‚
โ”‚                                                              โ”‚
โ”‚  Children inherit parent decorators.                         โ”‚
โ”‚  Siblings are isolated from each other.                      โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Request Lifecycle

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  REQUEST LIFECYCLE                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  Request arrives                                             โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  onRequest โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ request ID, timing                โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  preParsing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ body parsing                      โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  preValidation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ transform raw request             โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  VALIDATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Ajv validates schema              โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  preHandler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ authentication                   โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  HANDLER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ route logic                       โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  preSerialization โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ transform payload                 โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  onSend โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ add headers                       โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  Response sent                                               โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  onResponse โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ metrics, logging                  โ”‚
โ”‚                                                              โ”‚
โ”‚  onError runs at any point if an error occurs.               โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Schema Validation Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SCHEMA VALIDATION FLOW                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  Request arrives                                             โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  schema: {                                           โ”‚    โ”‚
โ”‚  โ”‚    body: { ... },                                    โ”‚    โ”‚
โ”‚  โ”‚    querystring: { ... },                             โ”‚    โ”‚
โ”‚  โ”‚    params: { ... },                                  โ”‚    โ”‚
โ”‚  โ”‚    headers: { ... }                                  โ”‚    โ”‚
โ”‚  โ”‚  }                                                   โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  Ajv validates each part                                     โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ”œโ”€โ”€ valid โ”€โ”€โ”€โ”€โ”€โ”€โ–บ handler receives typed data             โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ””โ”€โ”€ invalid โ”€โ”€โ”€โ–บ 400 error with validation details        โ”‚
โ”‚                                                              โ”‚
โ”‚  Response schema uses fast-json-stringify for serialization. โ”‚
โ”‚  It strips fields not in the schema.                         โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Hook Scoping

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  HOOK SCOPING                                                โ”‚
โ”‚                                                              โ”‚
โ”‚  app.addHook('preHandler', globalAuth)                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Applies to ALL routes in the application            โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  fastify.register(async (instance) => {                      โ”‚
โ”‚    instance.addHook('preHandler', scopedAuth)                โ”‚
โ”‚    instance.get('/protected', handler)                       โ”‚
โ”‚  })                                                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  scopedAuth applies ONLY to /protected               โ”‚    โ”‚
โ”‚  โ”‚  It does NOT apply to routes outside this plugin     โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  fastify.get('/route', {                                     โ”‚
โ”‚    preHandler: [routeAuth]                                   โ”‚
โ”‚  }, handler)                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  routeAuth applies ONLY to this route                โ”‚    โ”‚
โ”‚  โ”‚  It runs as the LAST preHandler hook                 โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Hook scope determines which routes the hook affects.        โ”‚
โ”‚  Register at the narrowest scope that covers the target.     โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
PluginAsync function registered with register()
EncapsulationChild contexts isolate decorators and hooks
fastify-pluginBreaks encapsulation; shares with parent
Lifecycle hooksonRequest, preValidation, preHandler, etc.
Hook scopeDetermined by the plugin where registered
Schema validatorsbody, query, params, headers
Validation engineAjv v8
Serializationfast-response schema with fast-json-stringify
Error handlingerror.validation and error.validationContext

Key takeaways:

  • Fastify plugins create encapsulated contexts. Every register() call creates a child context. A plugin’s decorators and hooks are visible to its descendants but not to its siblings or ancestors .
  • fastify-plugin deliberately breaks encapsulation. It makes a plugin’s additions visible to the parent scope. Use it only for cross-cutting concerns: database pools, loggers, shared configuration .
  • Hooks are scoped to the plugin where they are registered. A preHandler hook in a plugin runs only for routes in that plugin and its descendants. Register hooks at the narrowest scope that covers the target routes .
  • The request lifecycle has ordered phases. onRequest runs first, preHandler runs after validation, onResponse runs after the response is sent. Each hook has a specific purpose .
  • Schema validation happens before the handler. The body, querystring, params, and headers keys are validated by Ajv. Invalid input produces a 400 error with error.validation details .
  • Response schemas are for serialization, not validation. Fastify uses fast-json-stringify to serialize according to the schema. Fields not in the schema are stripped, which prevents accidental data disclosure .
  • Plugin registration order matters for async dependencies. Use .after() or await register() to ensure a plugin is loaded before its dependents .
  • The skip-override symbol is a footgun. It achieves the same effect as fastify-plugin without the guardrails. Fastify’s documentation explicitly labels it “not recommended” .

Remember: Fastify’s plugin system is built on encapsulation. Every register() call creates a new context, and a plugin’s decorators and hooks are isolated to that context and its descendants. This isolation prevents accidental coupling between unrelated plugins and makes the application’s dependency graph explicit. The fastify-plugin wrapper is the escape hatch for genuinely shared concerns, but it should be used sparingly and only for decoration. Hooks are the injection points into the request lifecycle, and their scope determines which routes they affect. Schema validation is the first line of defense, rejecting invalid input before it reaches the handler. The combination of encapsulated plugins, scoped hooks, and declarative schema validation is what makes a Fastify application both fast and maintainable.



Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!