Sharing State Across Fastify Plugins Without Breaking Encapsulation
Fastify encapsulates plugins by default. Learn when to use fastify-plugin to share decorators and connections, and how to verify the boundary.
01 Nov 2025, 00:57 UTC

The problem: your plugin’s decorator is invisible
You register a Fastify plugin that adds a database client to fastify.db. A route defined in the root instance tries to use request.server.db and gets undefined. Another plugin registers a hook with the same name, and nothing collides—until you move code around and suddenly it does.
This happens because Fastify plugins are encapsulated by default. Encapsulation means a plugin’s decorators, hooks, and routes live in a private scope. The parent application cannot see them, and sibling plugins cannot see each other. It is a feature that prevents name collisions, but it also means shared state does not share itself.
The engineering decision: when to use fastify-plugin
fastify-plugin is a utility that wraps a plugin so its encapsulated bindings are exposed to the parent scope. Use it when a plugin owns a resource that multiple parts of the application need, such as a database connection pool, a Redis client, or a custom decorator that adds user context to every request.
Do not use it for every plugin. A plugin that defines its own routes and keeps its decorators private should stay encapsulated. Wrapping everything with fastify-plugin defeats the isolation that makes Fastify plugins safe to compose.
Version assumption
This guidance assumes Fastify v4 or later. Earlier versions do not have the same encapsulation API, and fastify-plugin may behave unexpectedly.
Worked example: sharing a database decorator
Install the utility in your project root:
npm install fastify-plugin
No special permissions are required, but the Node process needs network access to reach the database host.
Create a file named db-plugin.js:
const fp = require('fastify-plugin');
const { Client } = require('pg');
async function dbPlugin(fastify, options) {
const client = new Client(options.connectionString);
await client.connect();
fastify.decorate('db', client);
fastify.addHook('onClose', async (instance, done) => {
await client.end();
done();
});
}
module.exports = fp(dbPlugin);
Register it in your application:
const fastify = require('fastify')({ logger: true });
const dbPlugin = require('./db-plugin');
fastify.register(dbPlugin, {
connectionString: process.env.DATABASE_URL || 'postgres://localhost:5432/app'
});
fastify.get('/users', async (request, reply) => {
const { rows } = await request.server.db.query('SELECT id, name FROM users LIMIT 10');
return reply.send(rows);
});
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err;
});
Expected check: start the server and call curl http://localhost:3000/users. If the decorator is shared correctly, the route returns rows instead of throwing TypeError: Cannot read properties of undefined.
Risk: if another plugin also decorates db and both are wrapped with fastify-plugin, Fastify may throw a duplicate decoration error at registration time. Check plugin names before wrapping.
Limitation: cleanup stays scoped unless you cross it
Encapsulation also limits lifecycle hooks. An onClose hook inside an encapsulated plugin only closes that plugin’s resources. When you wrap a plugin with fastify-plugin, its onClose runs in the parent scope, which is usually what you want for a shared database client. Verify by sending a SIGTERM to the running process and confirming the database pool ends without hanging.
Practical check: run kill -TERM <PID> against the server process and inspect the logs for the onClose completion message. If the process hangs, the shared resource may not be detached correctly.
Closing
Start with encapsulation. Only reach for fastify-plugin when a route, hook, or decorator genuinely needs to cross the plugin boundary. Register one wrapped plugin for shared infrastructure, keep domain plugins encapsulated, and verify with a request and a shutdown signal before deploying.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.