Fastify best practices skill
93
97%
Does it follow best practices?
Impact
85%
1.37xAverage score across 4 eval scenarios
Passed
No known issues
Fastify's plugin system provides automatic encapsulation. Each plugin creates its own context, isolating decorators, hooks, and plugins registered within it:
import Fastify from 'fastify';
import fp from 'fastify-plugin';
const app = Fastify();
// This plugin is encapsulated - its decorators are NOT available to siblings
app.register(async function childPlugin(fastify) {
fastify.decorate('privateUtil', () => 'only available here');
// This decorator is only available within this plugin and its children
fastify.get('/child', async function (request, reply) {
return this.privateUtil();
});
});
// This route CANNOT access privateUtil - it's in a different context
app.get('/parent', async function (request, reply) {
// this.privateUtil is undefined here
return { status: 'ok' };
});Use fastify-plugin when you need to share decorators, hooks, or plugins with the parent context:
import fp from 'fastify-plugin';
// This plugin's decorators will be available to the parent and siblings
export default fp(async function databasePlugin(fastify, options) {
const db = await createConnection(options.connectionString);
fastify.decorate('db', db);
fastify.addHook('onClose', async () => {
await db.close();
});
}, {
name: 'database-plugin',
dependencies: [], // List plugin dependencies
});Plugins are registered in order, but loading is asynchronous. Use after() for sequential dependencies:
import Fastify from 'fastify';
import databasePlugin from './plugins/database.js';
import authPlugin from './plugins/auth.js';
import routesPlugin from './routes/index.js';
const app = Fastify();
// Database must be ready before auth
app.register(databasePlugin);
// Auth depends on database
app.register(authPlugin);
// Routes depend on both
app.register(routesPlugin);
// Or use after() for explicit sequencing
app.register(databasePlugin).after(() => {
app.register(authPlugin).after(() => {
app.register(routesPlugin);
});
});
await app.ready();Always validate and document plugin options:
import fp from 'fastify-plugin';
interface CachePluginOptions {
ttl: number;
maxSize?: number;
prefix?: string;
}
export default fp<CachePluginOptions>(async function cachePlugin(fastify, options) {
const { ttl, maxSize = 1000, prefix = 'cache:' } = options;
if (typeof ttl !== 'number' || ttl <= 0) {
throw new Error('Cache plugin requires a positive ttl option');
}
const cache = new Map<string, { value: unknown; expires: number }>();
fastify.decorate('cache', {
get(key: string): unknown | undefined {
const item = cache.get(prefix + key);
if (!item) return undefined;
if (Date.now() > item.expires) {
cache.delete(prefix + key);
return undefined;
}
return item.value;
},
set(key: string, value: unknown): void {
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(prefix + key, { value, expires: Date.now() + ttl });
},
});
}, {
name: 'cache-plugin',
});Create configurable plugins using factory functions:
import fp from 'fastify-plugin';
interface RateLimitOptions {
max: number;
timeWindow: number;
}
function createRateLimiter(defaults: Partial<RateLimitOptions> = {}) {
return fp<RateLimitOptions>(async function rateLimitPlugin(fastify, options) {
const config = { ...defaults, ...options };
// Implementation
fastify.decorate('rateLimit', config);
}, {
name: 'rate-limiter',
});
}
// Usage
app.register(createRateLimiter({ max: 100 }), { timeWindow: 60000 });Declare dependencies to ensure proper load order:
import fp from 'fastify-plugin';
export default fp(async function authPlugin(fastify) {
// This plugin requires 'database-plugin' to be loaded first
if (!fastify.hasDecorator('db')) {
throw new Error('Auth plugin requires database plugin');
}
fastify.decorate('authenticate', async (request) => {
const user = await fastify.db.users.findByToken(request.headers.authorization);
return user;
});
}, {
name: 'auth-plugin',
dependencies: ['database-plugin'],
});Use encapsulation to scope plugins to specific routes:
import Fastify from 'fastify';
const app = Fastify();
// Public routes - no auth required
app.register(async function publicRoutes(fastify) {
fastify.get('/health', async () => ({ status: 'ok' }));
fastify.get('/docs', async () => ({ version: '1.0.0' }));
});
// Protected routes - auth required
app.register(async function protectedRoutes(fastify) {
// Auth hook only applies to routes in this plugin
fastify.addHook('onRequest', async (request, reply) => {
const token = request.headers.authorization;
if (!token) {
reply.code(401).send({ error: 'Unauthorized' });
return;
}
request.user = await verifyToken(token);
});
fastify.get('/profile', async (request) => {
return { user: request.user };
});
fastify.get('/settings', async (request) => {
return { settings: await getSettings(request.user.id) };
});
});Use the prefix option to namespace routes:
app.register(import('./routes/users.js'), { prefix: '/api/v1/users' });
app.register(import('./routes/posts.js'), { prefix: '/api/v1/posts' });
// In routes/users.js
export default async function userRoutes(fastify) {
// Becomes /api/v1/users
fastify.get('/', async () => {
return { users: [] };
});
// Becomes /api/v1/users/:id
fastify.get('/:id', async (request) => {
return { user: { id: request.params.id } };
});
}Add metadata for documentation and tooling:
import fp from 'fastify-plugin';
async function metricsPlugin(fastify) {
// Implementation
}
export default fp(metricsPlugin, {
name: 'metrics-plugin',
fastify: '5.x', // Fastify version compatibility
dependencies: ['pino-plugin'],
decorators: {
fastify: ['db'], // Required decorators
request: [],
reply: [],
},
});Use @fastify/autoload for automatic plugin loading:
import Fastify from 'fastify';
import autoload from '@fastify/autoload';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = Fastify();
// Load all plugins from the plugins directory
app.register(autoload, {
dir: join(__dirname, 'plugins'),
options: { prefix: '/api' },
});
// Load all routes from the routes directory
app.register(autoload, {
dir: join(__dirname, 'routes'),
options: { prefix: '/api' },
});Test plugins independently:
import { describe, it, before, after } from 'node:test';
import Fastify from 'fastify';
import myPlugin from './my-plugin.js';
describe('MyPlugin', () => {
let app;
before(async () => {
app = Fastify();
app.register(myPlugin, { option: 'value' });
await app.ready();
});
after(async () => {
await app.close();
});
it('should decorate fastify instance', (t) => {
t.assert.ok(app.hasDecorator('myDecorator'));
});
});