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
Always use env-schema for configuration validation. It provides JSON Schema validation for environment variables with sensible defaults.
import Fastify from 'fastify';
import envSchema from 'env-schema';
import { Type, type Static } from '@sinclair/typebox';
const schema = Type.Object({
PORT: Type.Number({ default: 3000 }),
HOST: Type.String({ default: '0.0.0.0' }),
DATABASE_URL: Type.String(),
JWT_SECRET: Type.String({ minLength: 32 }),
LOG_LEVEL: Type.Union([
Type.Literal('trace'),
Type.Literal('debug'),
Type.Literal('info'),
Type.Literal('warn'),
Type.Literal('error'),
Type.Literal('fatal'),
], { default: 'info' }),
});
type Config = Static<typeof schema>;
const config = envSchema<Config>({
schema,
dotenv: true, // Load from .env file
});
const app = Fastify({
logger: { level: config.LOG_LEVEL },
});
app.decorate('config', config);
declare module 'fastify' {
interface FastifyInstance {
config: Config;
}
}
await app.listen({ port: config.PORT, host: config.HOST });Encapsulate configuration in a plugin for reuse:
import fp from 'fastify-plugin';
import envSchema from 'env-schema';
import { Type, type Static } from '@sinclair/typebox';
const schema = Type.Object({
PORT: Type.Number({ default: 3000 }),
HOST: Type.String({ default: '0.0.0.0' }),
DATABASE_URL: Type.String(),
JWT_SECRET: Type.String({ minLength: 32 }),
LOG_LEVEL: Type.String({ default: 'info' }),
});
type Config = Static<typeof schema>;
declare module 'fastify' {
interface FastifyInstance {
config: Config;
}
}
export default fp(async function configPlugin(fastify) {
const config = envSchema<Config>({
schema,
dotenv: true,
});
fastify.decorate('config', config);
}, {
name: 'config',
});Handle secrets securely:
// Never log secrets
const app = Fastify({
logger: {
level: config.LOG_LEVEL,
redact: ['req.headers.authorization', '*.password', '*.secret', '*.apiKey'],
},
});
// For production, use secret managers (AWS Secrets Manager, Vault, etc.)
// Pass secrets through environment variables - never commit themImplement feature flags via environment variables:
import { Type, type Static } from '@sinclair/typebox';
const schema = Type.Object({
// ... other config
FEATURE_NEW_DASHBOARD: Type.Boolean({ default: false }),
FEATURE_BETA_API: Type.Boolean({ default: false }),
});
type Config = Static<typeof schema>;
const config = envSchema<Config>({ schema, dotenv: true });
// Use in routes
app.get('/dashboard', async (request) => {
if (app.config.FEATURE_NEW_DASHBOARD) {
return { version: 'v2', data: await getNewDashboardData() };
}
return { version: 'v1', data: await getOldDashboardData() };
});// ❌ NEVER DO THIS - configuration files are an antipattern
import config from './config/production.json';
// ❌ NEVER DO THIS - per-environment config files
const env = process.env.NODE_ENV || 'development';
const config = await import(`./config/${env}.js`);Configuration files lead to:
// ❌ NEVER DO THIS
const configs = {
development: { logLevel: 'debug' },
production: { logLevel: 'info' },
test: { logLevel: 'silent' },
};
const config = configs[process.env.NODE_ENV];Instead, use a single configuration source (environment variables) with sensible defaults. The environment controls the values, not conditional code.
// ❌ AVOID checking NODE_ENV
if (process.env.NODE_ENV === 'production') {
// do something
}
// ✅ BETTER - use explicit feature flags or configuration
if (app.config.ENABLE_DETAILED_LOGGING) {
// do something
}For configuration that needs to change without restart, fetch from an external service:
interface DynamicConfig {
rateLimit: number;
maintenanceMode: boolean;
}
let dynamicConfig: DynamicConfig = {
rateLimit: 100,
maintenanceMode: false,
};
async function refreshConfig() {
try {
const newConfig = await fetchConfigFromService();
dynamicConfig = newConfig;
app.log.info('Configuration refreshed');
} catch (error) {
app.log.error({ err: error }, 'Failed to refresh configuration');
}
}
// Refresh periodically
setInterval(refreshConfig, 60000);
// Use in hooks
app.addHook('onRequest', async (request, reply) => {
if (dynamicConfig.maintenanceMode && !request.url.startsWith('/health')) {
reply.code(503).send({ error: 'Service under maintenance' });
}
});