CtrlK
BlogDocsLog inGet started
Tessl Logo

mcollina/fastify-best-practices

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

Quality

95%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

configuration.mdrules/

name:
configuration
description:
Application configuration in Fastify using env-schema
metadata:
{"tags":"configuration, environment, env, settings, env-schema"}

Application Configuration

Use env-schema for Configuration

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 });

Configuration as Plugin

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',
});

Secrets Management

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 them

Feature Flags

Implement 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() };
});

Anti-Patterns to Avoid

NEVER use configuration files

// ❌ 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:

  • Security risks (secrets in files)
  • Deployment complexity
  • Environment drift
  • Difficult secret rotation

NEVER use per-environment configuration

// ❌ 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.

Use specific environment variables, not NODE_ENV

// ❌ 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
}

Dynamic Configuration

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' });
  }
});

rules

authentication.md

configuration.md

content-type.md

cors-security.md

database.md

decorators.md

deployment.md

error-handling.md

hooks.md

http-proxy.md

logging.md

performance.md

plugins.md

routes.md

schemas.md

serialization.md

testing.md

typescript.md

websockets.md

SKILL.md

tile.json