Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts.
95
95%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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'));
});
});