30.5 Fastify Plugins and the Typed Plugin Pattern
Alright, let’s talk about Fastify plugins. This is where Fastify truly flexes its architectural muscles and leaves other frameworks looking a bit… basic. The plugin system isn’t just a way to add features; it’s the fundamental, first-class way you build anything in Fastify. It’s how you encapsulate functionality, manage dependencies, and—crucially for us—how we get TypeScript to understand our entire application’s structure.
Think of a Fastify plugin as a neatly wrapped package containing routes, decorators, and hooks. The magic is that these packages can be developed, tested, and reasoned about in isolation before being seamlessly integrated into your main application. It’s the opposite of the “just attach stuff to the app object and pray” pattern you might be used to.
The Anatomy of a Basic Plugin
At its core, a Fastify plugin is just a function. But it’s a function that follows a very specific signature. Here’s the simplest “hello world” of plugins:
// greetings-plugin.ts
import { FastifyPluginAsync } from 'fastify';
// Define the options interface for your plugin. This is optional but highly recommended.
export interface GreetingPluginOptions {
defaultName: string;
}
// Use the `FastifyPluginAsync` type for an async function. Use `FastifyPluginCallback` for callback style.
const greetingsPlugin: FastifyPluginAsync<GreetingPluginOptions> = async (
fastify, // The Fastify instance this plugin is being loaded into
options // The options specific to this plugin
) => {
// Let's add a route within our plugin's scope
fastify.get('/hello', async (request, reply) => {
return { hello: options.defaultName || 'world' };
});
// Maybe we also want to decorate the fastify instance with a utility function?
fastify.decorate('generateGreeting', (name: string) => `Hello, ${name}!`);
};
// Don't forget this! It makes your plugin discoverable by Fastify.
export default greetingsPlugin;
To use this beautiful creation:
// app.ts
import Fastify from 'fastify';
import greetingsPlugin from './greetings-plugin';
const app = Fastify();
// Register the plugin with its required options
await app.register(greetingsPlugin, { defaultName: 'TypeScript Wizard' });
await app.listen({ port: 3000 });
Now, hit http://localhost:3000/hello and you’ll get {"hello":"TypeScript Wizard"}. Brilliant. But we’re here for the types, so let’s address the elephant in the room.
The TypeScript Decoration Problem
See that fastify.decorate('generateGreeting', ...) line? We just added a new method to the Fastify instance. This is incredibly powerful, but TypeScript’s type system has no idea it exists. If you try to use app.generateGreeting('Reader') in your app.ts file, the TypeScript compiler will immediately throw a tantrum, saying “Property ‘generateGreeting’ does not exist on type FastifyInstance…”. Rude.
This is the central challenge of typing Fastify plugins: we need to tell TypeScript about the new “shape” of our Fastify instance after the plugin has done its decoration. This is where the fastify-plugin and declaration merging come in, a combination I like to call the “Typed Plugin Pattern.”
The Typed Plugin Pattern: Making TypeScript Play Along
The solution is a two-part dance. First, we define the types our plugin adds. Second, we use fastify-plugin to ensure our plugin inherits the types of the parent context instead of creating a new one (which is the default, and usually unhelpful, behavior).
Let’s fix our previous example.
Step 1: Create a Type Definition File
We create a types file to use TypeScript’s declaration merging. This is where we teach TypeScript about our new decorations.
// types/fastify.d.ts
import { GreetingPluginOptions } from '../greetings-plugin';
// 1. Define the interface for the new stuff we're adding
declare module 'fastify' {
interface FastifyInstance {
generateGreeting: (name: string) => string;
}
// 2. (Optional) If you want to access your plugin options on the instance
interface FastifyInstance {
config: GreetingPluginOptions; // Or a more complex config object
}
}
Step 2: Refactor the Plugin with fastify-plugin
Now we modify the plugin itself to use the fastify-plugin wrapper.
// greetings-plugin.ts
import fp from 'fastify-plugin';
import { FastifyPluginAsync } from 'fastify';
export interface GreetingPluginOptions {
defaultName: string;
}
// Notice we now wrap our plugin function with `fp`
const greetingsPlugin: FastifyPluginAsync<GreetingPluginOptions> = fp(async (fastify, options) => {
fastify.decorate('generateGreeting', (name: string) => `Hello, ${name}!`);
// You can now also assign options to the instance if you want
fastify.decorate('config', options);
fastify.get('/hello', async (request, reply) => {
// Now we can use our decorated method, fully type-safe within the plugin!
const greeting = fastify.generateGreeting(options.defaultName);
return { message: greeting };
});
});
export default greetingsPlugin;
Why fp? By default, Fastify creates a new scoped context for each plugin. This is great for encapsulation but terrible for type propagation. The fastify-plugin wrapper tells Fastify to not create a new scope, meaning your decorations are added to the same context your plugin was registered in. This is almost always what you want for type sharing.
Now, back in your app.ts, TypeScript will finally shut up and behave. It knows that app.generateGreeting is a valid method that takes a string.
// app.ts is now fully type-safe!
console.log(app.generateGreeting('Reader')); // No compiler errors!
Best Practices and Pitfalls
- Always Use
fpfor Typed Plugins: If you’re decorating the instance and care about types, you wantfastify-plugin. Use it by default. - Declaration Merging is Non-Negotiable: The
decoratemethod only affects runtime. Without the correspondingdeclare moduleblock in your type definition file, TypeScript remains blissfully unaware of your changes. You must do both. - Order Matters: Plugins are loaded in the order they are registered. You can’t use a decorator from
PluginBinsidePluginAifPluginAis registered first. Plan your dependency tree and use theawaitkeyword when registering to control this. - Don’t Over-Decorate: The decorator pattern is powerful, but don’t turn your Fastify instance into a junk drawer. Decorate with purpose—utility functions, pre-configured clients (like a database connection), or validated config objects are perfect candidates.