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 :
onRequestโ The first hook. Runs before any parsing. Used for request ID generation, timing, and logging .preParsingโ Runs before the body is parsed. Used for custom content-type handling.preValidationโ Runs before validation. Used for transforming the raw request.preHandlerโ Runs after validation, before the handler. Used for authentication and authorization .- Handler โ The route’s handler function.
preSerializationโ Runs before the response is serialized. Used for transforming the payload.onSendโ Runs before the response is sent. Used for adding headers.onResponseโ Runs after the response is sent. Used for metrics and logging .onErrorโ Runs when an error occurs.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
| API | Purpose |
|---|---|
fastify.register(plugin, options) | Register a plugin in a child context |
fastify-plugin wrapper | Break 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
| Hook | Phase | Use case |
|---|---|---|
onRequest | First | Request ID, timing, logging |
preParsing | Before body parse | Custom content types |
preValidation | Before validation | Transform raw request |
preHandler | After validation | Authentication, authorization |
preSerialization | Before serialize | Transform payload |
onSend | Before send | Add headers |
onResponse | After response | Metrics, logging |
onError | On error | Error handling |
onTimeout | On timeout | Cleanup |
Schema Validators
| Key | Validates |
|---|---|
body | POST/PUT/PATCH body |
querystring or query | Query string |
params | Route parameters |
headers | Request headers |
response | Response serialization (not validation) |
Encapsulation Rules
| Rule | Behavior |
|---|---|
| Child sees parent | โ Parent decorators available |
| Parent sees child | โ Child decorators not available |
| Sibling sees sibling | โ Isolated |
fastify-plugin | Breaks 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Decorator not available in sibling | Encapsulation isolation | Use fastify-plugin or restructure |
| Auth hook applies to wrong routes | Hook registered at wrong scope | Register in the plugin that needs it |
| Validation not working | Schema missing or wrong key | Check body/query/params keys |
| Response includes sensitive fields | No response schema | Add response schema |
| Plugin order matters | Async loading | Use .after() for sequential dependencies |
this is undefined in arrow hooks | Arrow functions don’t bind this | Use regular functions |
| Custom content type not validated | No content key in schema | Add 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
| Item | Value |
|---|---|
| Plugin | Async function registered with register() |
| Encapsulation | Child contexts isolate decorators and hooks |
fastify-plugin | Breaks encapsulation; shares with parent |
| Lifecycle hooks | onRequest, preValidation, preHandler, etc. |
| Hook scope | Determined by the plugin where registered |
| Schema validators | body, query, params, headers |
| Validation engine | Ajv v8 |
| Serialization | fast-response schema with fast-json-stringify |
| Error handling | error.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-plugindeliberately 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
preHandlerhook 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.
onRequestruns first,preHandlerruns after validation,onResponseruns after the response is sent. Each hook has a specific purpose . - Schema validation happens before the handler. The
body,querystring,params, andheaderskeys are validated by Ajv. Invalid input produces a 400 error witherror.validationdetails . - Response schemas are for serialization, not validation. Fastify uses
fast-json-stringifyto 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()orawait register()to ensure a plugin is loaded before its dependents . - The
skip-overridesymbol is a footgun. It achieves the same effect asfastify-pluginwithout 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!