Fastify best practices skill
93
97%
Does it follow best practices?
Impact
85%
1.37xAverage score across 4 eval scenarios
Passed
No known issues
Enable Cross-Origin Resource Sharing:
import Fastify from 'fastify';
import cors from '@fastify/cors';
const app = Fastify();
// Simple CORS - allow all origins
app.register(cors);
// Configured CORS
app.register(cors, {
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
credentials: true,
maxAge: 86400, // 24 hours
});Validate origins dynamically:
app.register(cors, {
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) {
return callback(null, true);
}
// Check against allowed origins
const allowedOrigins = [
'https://example.com',
'https://app.example.com',
/\.example\.com$/,
];
const isAllowed = allowedOrigins.some((allowed) => {
if (allowed instanceof RegExp) {
return allowed.test(origin);
}
return allowed === origin;
});
if (isAllowed) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'), false);
}
},
credentials: true,
});Configure CORS for specific routes:
app.register(cors, {
origin: true, // Reflect request origin
credentials: true,
});
// Or disable CORS for specific routes
app.route({
method: 'GET',
url: '/internal',
config: {
cors: false,
},
handler: async () => {
return { internal: true };
},
});Add security headers:
import helmet from '@fastify/helmet';
app.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.example.com'],
},
},
crossOriginEmbedderPolicy: false, // Disable if embedding external resources
});Fine-tune security headers:
app.register(helmet, {
// Strict Transport Security
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
// Content Security Policy
contentSecurityPolicy: {
useDefaults: true,
directives: {
'script-src': ["'self'", 'https://trusted-cdn.com'],
},
},
// X-Frame-Options
frameguard: {
action: 'deny', // or 'sameorigin'
},
// X-Content-Type-Options
noSniff: true,
// X-XSS-Protection (legacy)
xssFilter: true,
// Referrer-Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin',
},
// X-Permitted-Cross-Domain-Policies
permittedCrossDomainPolicies: false,
// X-DNS-Prefetch-Control
dnsPrefetchControl: {
allow: false,
},
});Protect against abuse:
import rateLimit from '@fastify/rate-limit';
app.register(rateLimit, {
max: 100,
timeWindow: '1 minute',
errorResponseBuilder: (request, context) => ({
statusCode: 429,
error: 'Too Many Requests',
message: `Rate limit exceeded. Retry in ${context.after}`,
retryAfter: context.after,
}),
});
// Per-route rate limit
app.get('/expensive', {
config: {
rateLimit: {
max: 10,
timeWindow: '1 minute',
},
},
}, handler);
// Skip rate limit for certain routes
app.get('/health', {
config: {
rateLimit: false,
},
}, () => ({ status: 'ok' }));Use Redis for distributed rate limiting:
import rateLimit from '@fastify/rate-limit';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
app.register(rateLimit, {
max: 100,
timeWindow: '1 minute',
redis,
nameSpace: 'rate-limit:',
keyGenerator: (request) => {
// Rate limit by user ID if authenticated, otherwise by IP
return request.user?.id || request.ip;
},
});Protect against Cross-Site Request Forgery:
import fastifyCsrf from '@fastify/csrf-protection';
import fastifyCookie from '@fastify/cookie';
app.register(fastifyCookie);
app.register(fastifyCsrf, {
cookieOpts: {
signed: true,
httpOnly: true,
sameSite: 'strict',
},
});
// Generate token
app.get('/csrf-token', async (request, reply) => {
const token = reply.generateCsrf();
return { token };
});
// Protected route
app.post('/transfer', {
preHandler: app.csrfProtection,
}, async (request) => {
// CSRF token validated
return { success: true };
});Add custom headers:
app.addHook('onSend', async (request, reply) => {
// Custom security headers
reply.header('X-Request-ID', request.id);
reply.header('X-Content-Type-Options', 'nosniff');
reply.header('X-Frame-Options', 'DENY');
reply.header('Permissions-Policy', 'geolocation=(), camera=()');
});
// Per-route headers
app.get('/download', async (request, reply) => {
reply.header('Content-Disposition', 'attachment; filename="file.pdf"');
reply.header('X-Download-Options', 'noopen');
return reply.send(fileStream);
});Configure secure cookies:
import cookie from '@fastify/cookie';
app.register(cookie, {
secret: process.env.COOKIE_SECRET,
parseOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
maxAge: 3600, // 1 hour
},
});
// Set secure cookie
app.post('/login', async (request, reply) => {
const token = await createSession(request.body);
reply.setCookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
maxAge: 86400,
signed: true,
});
return { success: true };
});
// Read signed cookie
app.get('/profile', async (request) => {
const session = request.cookies.session;
const unsigned = request.unsignCookie(session);
if (!unsigned.valid) {
throw { statusCode: 401, message: 'Invalid session' };
}
return { sessionId: unsigned.value };
});Validate and sanitize input:
// Schema-based validation protects against injection
app.post('/users', {
schema: {
body: {
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
maxLength: 254,
},
name: {
type: 'string',
minLength: 1,
maxLength: 100,
pattern: '^[a-zA-Z\\s]+$', // Only letters and spaces
},
},
required: ['email', 'name'],
additionalProperties: false,
},
},
}, handler);Restrict access by IP:
const allowedIps = new Set([
'192.168.1.0/24',
'10.0.0.0/8',
]);
app.addHook('onRequest', async (request, reply) => {
if (request.url.startsWith('/admin')) {
const clientIp = request.ip;
if (!isIpAllowed(clientIp, allowedIps)) {
reply.code(403).send({ error: 'Forbidden' });
}
}
});
function isIpAllowed(ip: string, allowed: Set<string>): boolean {
// Implement IP/CIDR matching
for (const range of allowed) {
if (ipInRange(ip, range)) return true;
}
return false;
}Configure for reverse proxy environments:
const app = Fastify({
trustProxy: true, // Trust X-Forwarded-* headers
});
// Or specific proxy configuration
const app = Fastify({
trustProxy: ['127.0.0.1', '10.0.0.0/8'],
});
// Now request.ip returns the real client IP
app.get('/ip', async (request) => {
return {
ip: request.ip,
ips: request.ips, // Array of all IPs in chain
};
});Force HTTPS in production:
app.addHook('onRequest', async (request, reply) => {
if (
process.env.NODE_ENV === 'production' &&
request.headers['x-forwarded-proto'] !== 'https'
) {
const httpsUrl = `https://${request.hostname}${request.url}`;
reply.redirect(301, httpsUrl);
}
});import Fastify from 'fastify';
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
import rateLimit from '@fastify/rate-limit';
const app = Fastify({
trustProxy: true,
bodyLimit: 1048576, // 1MB max body
});
// Security plugins
app.register(helmet);
app.register(cors, {
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true,
});
app.register(rateLimit, {
max: 100,
timeWindow: '1 minute',
});
// Validate all input with schemas
// Never expose internal errors in production
// Use parameterized queries for database
// Keep dependencies updated