Skills로 돌아가기
mcollina/skills실행 전 동작 확인

SKILL DETAIL

fastify-best-practices

mcollina/skills/fastify-best-practices

This skill provides comprehensive guidance for building Node.js backend servers and REST APIs with Fastify, supporting both TypeScript and JavaScript. It covers the full development lifecycle of a Fastify application, including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimizing performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. The skill details the Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. It offers best practices and code examples to help developers adhere to Fastify's core principles of encapsulation, schema-first design, and performance optimization, enabling the creation of efficient and maintainable server applications.

설치 수 · 387출처 보기

Installation

npx skills add https://github.com/mcollina/skills --skill fastify-best-practices

스킬 파일

SKILL.md

최근 동기화 · 2026. 8. 29.

rules/authentication.md
---
name: authentication
description: Authentication and authorization patterns in Fastify
metadata:
  tags: auth, jwt, session, oauth, security, authorization
---

# Authentication and Authorization

## Contents

- [JWT Authentication with @fastify/jwt](#jwt-authentication-with-fastifyjwt)
- [Refresh Tokens](#refresh-tokens)
- [Role-Based Access Control](#role-based-access-control)
- [Permission-Based Authorization](#permission-based-authorization)
- [API Key / Bearer Token Authentication](#api-key--bearer-token-authentication)
- [OAuth 2.0 Integration](#oauth-20-integration)
- [Session-Based Authentication](#session-based-authentication)
- [Resource-Based Authorization](#resource-based-authorization)
- [Password Hashing](#password-hashing)
- [Rate Limiting for Auth Endpoints](#rate-limiting-for-auth-endpoints)

## JWT Authentication with @fastify/jwt

Use `@fastify/jwt` for JSON Web Token authentication:

```typescript
import Fastify from 'fastify';
import fastifyJwt from '@fastify/jwt';

const app = Fastify();

app.register(fastifyJwt, {
  secret: process.env.JWT_SECRET,
  sign: {
    expiresIn: '1h',
  },
});

// Decorate request with authentication method
app.decorate('authenticate', async function (request, reply) {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.code(401).send({ error: 'Unauthorized' });
  }
});

// Login route
app.post('/login', {
  schema: {
    body: {
      type: 'object',
      properties: {
        email: { type: 'string', format: 'email' },
        password: { type: 'string' },
      },
      required: ['email', 'password'],
    },
  },
}, async (request, reply) => {
  const { email, password } = request.body;
  const user = await validateCredentials(email, password);

  if (!user) {
    return reply.code(401).send({ error: 'Invalid credentials' });
  }

  const token = app.jwt.sign({
    id: user.id,
    email: user.email,
    role: user.role,
  });

  return { token };
});

// Protected route
app.get('/profile', {
  onRequest: [app.authenticate],
}, async (request) => {
  return { user: request.user };
});
```

## Refresh Tokens

Implement refresh token rotation:

```typescript
import fastifyJwt from '@fastify/jwt';
import { randomBytes } from 'node:crypto';

app.register(fastifyJwt, {
  secret: process.env.JWT_SECRET,
  sign: {
    expiresIn: '15m', // Short-lived access tokens
  },
});

// Store refresh tokens (use Redis in production)
const refreshTokens = new Map<string, { userId: string; expires: number }>();

app.post('/auth/login', async (request, reply) => {
  const { email, password } = request.body;
  const user = await validateCredentials(email, password);

  if (!user) {
    return reply.code(401).send({ error: 'Invalid credentials' });
  }

  const accessToken = app.jwt.sign({ id: user.id, role: user.role });
  const refreshToken = randomBytes(32).toString('hex');

  refreshTokens.set(refreshToken, {
    userId: user.id,
    expires: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days
  });

  return { accessToken, refreshToken };
});

app.post('/auth/refresh', async (request, reply) => {
  const { refreshToken } = request.body;
  const stored = refreshTokens.get(refreshToken);

  if (!stored || stored.expires < Date.now()) {
    refreshTokens.delete(refreshToken);
    return reply.code(401).send({ error: 'Invalid refresh token' });
  }

  // Delete old token (rotation)
  refreshTokens.delete(refreshToken);

  const user = await db.users.findById(stored.userId);
  const accessToken = app.jwt.sign({ id: user.id, role: user.role });
  const newRefreshToken = randomBytes(32).toString('hex');

  refreshTokens.set(newRefreshToken, {
    userId: user.id,
    expires: Date.now() + 7 * 24 * 60 * 60 * 1000,
  });

  return { accessToken, refreshToken: newRefreshToken };
});

app.post('/auth/logout', async (request, reply) => {
  const { refreshToken } = request.body;
  refreshTokens.delete(refreshToken);
  return { success: true };
});
```

## Role-Based Access Control

Implement RBAC with decorators:

```typescript
type Role = 'admin' | 'user' | 'moderator';

// Create authorization decorator
app.decorate('authorize', function (...allowedRoles: Role[]) {
  return async (request, reply) => {
    await request.jwtVerify();

    const userRole = request.user.role as Role;
    if (!allowedRoles.includes(userRole)) {
      return reply.code(403).send({
        error: 'Forbidden',
        message: `Role '${userRole}' is not authorized for this resource`,
      });
    }
  };
});

// Admin only route
app.get('/admin/users', {
  onRequest: [app.authorize('admin')],
}, async (request) => {
  return db.users.findAll();
});

// Admin or moderator
app.delete('/posts/:id', {
  onRequest: [app.authorize('admin', 'moderator')],
}, async (request) => {
  await db.posts.delete(request.params.id);
  return { deleted: true };
});
```

## Permission-Based Authorization

Fine-grained permission checks:

```typescript
interface Permission {
  resource: string;
  action: 'create' | 'read' | 'update' | 'delete';
}

const rolePermissions: Record<string, Permission[]> = {
  admin: [
    { resource: '*', action: 'create' },
    { resource: '*', action: 'read' },
    { resource: '*', action: 'update' },
    { resource: '*', action: 'delete' },
  ],
  user: [
    { resource: 'posts', action: 'create' },
    { resource: 'posts', action: 'read' },
    { resource: 'comments', action: 'create' },
    { resource: 'comments', action: 'read' },
  ],
};

function hasPermission(role: string, resource: string, action: string): boolean {
  const permissions = rolePermissions[role] || [];
  return permissions.some(
    (p) =>
      (p.resource === '*' || p.resource === resource) &&
      p.action === action
  );
}

app.decorate('checkPermission', function (resource: string, action: string) {
  return async (request, reply) => {
    await request.jwtVerify();

    if (!hasPermission(request.user.role, resource, action)) {
      return reply.code(403).send({
        error: 'Forbidden',
        message: `Not allowed to ${action} ${resource}`,
      });
    }
  };
});

// Usage
app.post('/posts', {
  onRequest: [app.checkPermission('posts', 'create')],
}, createPostHandler);

app.delete('/posts/:id', {
  onRequest: [app.checkPermission('posts', 'delete')],
}, deletePostHandler);
```

## API Key / Bearer Token Authentication

Use `@fastify/bearer-auth` for API key and bearer token authentication:

```typescript
import bearerAuth from '@fastify/bearer-auth';

const validKeys = new Set([process.env.API_KEY]);

app.register(bearerAuth, {
  keys: validKeys,
  errorResponse: (err) => ({
    error: 'Unauthorized',
    message: 'Invalid API key',
  }),
});

// All routes are now protected
app.get('/api/data', async (request) => {
  return { data: [] };
});
```

For database-backed API keys with custom validation:

```typescript
import bearerAuth from '@fastify/bearer-auth';

app.register(bearerAuth, {
  auth: async (key, request) => {
    const apiKey = await db.apiKeys.findByKey(key);

    if (!apiKey || !apiKey.active) {
      return false;
    }

    // Track usage (fire and forget)
    db.apiKeys.recordUsage(apiKey.id, {
      ip: request.ip,
      timestamp: new Date(),
    });

    request.apiKey = apiKey;
    return true;
  },
  errorResponse: (err) => ({
    error: 'Unauthorized',
    message: 'Invalid API key',
  }),
});
```

## OAuth 2.0 Integration

Integrate with OAuth providers using @fastify/oauth2:

```typescript
import fastifyOauth2 from '@fastify/oauth2';

app.register(fastifyOauth2, {
  name: 'googleOAuth2',
  scope: ['profile', 'email'],
  credentials: {
    client: {
      id: process.env.GOOGLE_CLIENT_ID,
      secret: process.env.GOOGLE_CLIENT_SECRET,
    },
  },
  startRedirectPath: '/auth/google',
  callbackUri: 'http://localhost:3000/auth/google/callback',
  discovery: {
    issuer: 'https://accounts.google.com',
  },
});

app.get('/auth/google/callback', async (request, reply) => {
  const { token } = await app.googleOAuth2.getAccessTokenFromAuthorizationCodeFlow(request);

  // Fetch user info from Google
  const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { Authorization: `Bearer ${token.access_token}` },
  }).then((r) => r.json());

  // Find or create user
  let user = await db.users.findByEmail(userInfo.email);
  if (!user) {
    user = await db.users.create({
      email: userInfo.email,
      name: userInfo.name,
      provider: 'google',
      providerId: userInfo.id,
    });
  }

  // Generate JWT
  const jwt = app.jwt.sign({ id: user.id, role: user.role });

  // Redirect to frontend with token
  return reply.redirect(`/auth/success?token=${jwt}`);
});
```

## Session-Based Authentication

Use @fastify/session for session management:

```typescript
import fastifyCookie from '@fastify/cookie';
import fastifySession from '@fastify/session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.register(fastifyCookie);
app.register(fastifySession, {
  secret: process.env.SESSION_SECRET,
  store: new RedisStore({ client: redisClient }),
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 1 day
  },
});

app.post('/login', async (request, reply) => {
  const { email, password } = request.body;
  const user = await validateCredentials(email, password);

  if (!user) {
    return reply.code(401).send({ error: 'Invalid credentials' });
  }

  request.session.userId = user.id;
  request.session.role = user.role;

  return { success: true };
});

app.decorate('requireSession', async function (request, reply) {
  if (!request.session.userId) {
    return reply.code(401).send({ error: 'Not authenticated' });
  }
});

app.get('/profile', {
  onRequest: [app.requireSession],
}, async (request) => {
  const user = await db.users.findById(request.session.userId);
  return { user };
});

app.post('/logout', async (request, reply) => {
  await request.session.destroy();
  return { success: true };
});
```

## Resource-Based Authorization

Check ownership of resources:

```typescript
app.decorate('checkOwnership', function (getResourceOwnerId: (request) => Promise<string>) {
  return async (request, reply) => {
    const ownerId = await getResourceOwnerId(request);

    if (ownerId !== request.user.id && request.user.role !== 'admin') {
      return reply.code(403).send({
        error: 'Forbidden',
        message: 'You do not own this resource',
      });
    }
  };
});

// Check post ownership
app.put('/posts/:id', {
  onRequest: [
    app.authenticate,
    app.checkOwnership(async (request) => {
      const post = await db.posts.findById(request.params.id);
      return post?.authorId;
    }),
  ],
}, updatePostHandler);

// Alternative: inline check
app.put('/posts/:id', {
  onRequest: [app.authenticate],
}, async (request, reply) => {
  const post = await db.posts.findById(request.params.id);

  if (!post) {
    return reply.code(404).send({ error: 'Post not found' });
  }

  if (post.authorId !== request.user.id && request.user.role !== 'admin') {
    return reply.code(403).send({ error: 'Forbidden' });
  }

  return db.posts.update(post.id, request.body);
});
```

## Password Hashing

Use secure password hashing with argon2:

```typescript
import { hash, verify } from '@node-rs/argon2';

async function hashPassword(password: string): Promise<string> {
  return hash(password, {
    memoryCost: 65536,
    timeCost: 3,
    parallelism: 4,
  });
}

async function verifyPassword(hash: string, password: string): Promise<boolean> {
  return verify(hash, password);
}

app.post('/register', async (request, reply) => {
  const { email, password } = request.body;

  const hashedPassword = await hashPassword(password);
  const user = await db.users.create({
    email,
    password: hashedPassword,
  });

  reply.code(201);
  return { id: user.id, email: user.email };
});

app.post('/login', async (request, reply) => {
  const { email, password } = request.body;
  const user = await db.users.findByEmail(email);

  if (!user || !(await verifyPassword(user.password, password))) {
    return reply.code(401).send({ error: 'Invalid credentials' });
  }

  const token = app.jwt.sign({ id: user.id, role: user.role });
  return { token };
});
```

## Rate Limiting for Auth Endpoints

Protect auth endpoints from brute force. **IMPORTANT: For production security, you MUST configure rate limiting with a Redis backend.** In-memory rate limiting is not safe for distributed deployments and can be bypassed.

```typescript
import fastifyRateLimit from '@fastify/rate-limit';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

// Global rate limit with Redis backend
app.register(fastifyRateLimit, {
  max: 100,
  timeWindow: '1 minute',
  redis, // REQUIRED for production - ensures rate limiting works across all instances
});

// Stricter limit for auth endpoints
app.register(async function authRoutes(fastify) {
  await fastify.register(fastifyRateLimit, {
    max: 5,
    timeWindow: '1 minute',
    redis, // REQUIRED for production
    keyGenerator: (request) => {
      // Rate limit by IP + email combination
      const email = request.body?.email || '';
      return `${request.ip}:${email}`;
    },
  });

  fastify.post('/login', loginHandler);
  fastify.post('/register', registerHandler);
  fastify.post('/forgot-password', forgotPasswordHandler);
}, { prefix: '/auth' });
```
rules/configuration.md
---
name: configuration
description: Application configuration in Fastify using env-schema
metadata:
  tags: configuration, environment, env, settings, env-schema
---

# Application Configuration

## Contents

- [Use env-schema for Configuration](#use-env-schema-for-configuration)
- [Configuration as Plugin](#configuration-as-plugin)
- [Secrets Management](#secrets-management)
- [Feature Flags](#feature-flags)
- [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
- [Dynamic Configuration](#dynamic-configuration)

## Use env-schema for Configuration

**Always use `env-schema` for configuration validation.** It provides JSON Schema validation for environment variables with sensible defaults.

```typescript
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:

```typescript
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:

```typescript
// 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:

```typescript
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

```typescript
// ❌ 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

```typescript
// ❌ 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

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

```typescript
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/content-type.md
---
name: content-type
description: Content type parsing in Fastify
metadata:
  tags: content-type, parsing, body, multipart, json
---

# Content Type Parsing

## Contents

- [Default Content Type Parsers](#default-content-type-parsers)
- [Custom Content Type Parsers](#custom-content-type-parsers)
- [XML Parsing](#xml-parsing)
- [Multipart Form Data](#multipart-form-data)
- [Stream Processing](#stream-processing)
- [Custom JSON Parser](#custom-json-parser)
- [Content Type with Parameters](#content-type-with-parameters)
- [Catch-All Parser](#catch-all-parser)
- [Body Limit Configuration](#body-limit-configuration)
- [Protocol Buffers](#protocol-buffers)
- [Form Data with @fastify/formbody](#form-data-with-fastifyformbody)
- [Content Negotiation](#content-negotiation)
- [Validation After Parsing](#validation-after-parsing)

## Default Content Type Parsers

Fastify includes parsers for common content types:

```typescript
import Fastify from 'fastify';

const app = Fastify();

// Built-in parsers:
// - application/json
// - text/plain

app.post('/json', async (request) => {
  // request.body is parsed JSON object
  return { received: request.body };
});

app.post('/text', async (request) => {
  // request.body is string for text/plain
  return { text: request.body };
});
```

## Custom Content Type Parsers

Add parsers for additional content types:

```typescript
// Parse application/x-www-form-urlencoded
app.addContentTypeParser(
  'application/x-www-form-urlencoded',
  { parseAs: 'string' },
  (request, body, done) => {
    const parsed = new URLSearchParams(body);
    done(null, Object.fromEntries(parsed));
  },
);

// Async parser
app.addContentTypeParser(
  'application/x-www-form-urlencoded',
  { parseAs: 'string' },
  async (request, body) => {
    const parsed = new URLSearchParams(body);
    return Object.fromEntries(parsed);
  },
);
```

## XML Parsing

Parse XML content:

```typescript
import { XMLParser } from 'fast-xml-parser';

const xmlParser = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: '@_',
});

app.addContentTypeParser(
  'application/xml',
  { parseAs: 'string' },
  async (request, body) => {
    return xmlParser.parse(body);
  },
);

app.addContentTypeParser(
  'text/xml',
  { parseAs: 'string' },
  async (request, body) => {
    return xmlParser.parse(body);
  },
);

app.post('/xml', async (request) => {
  // request.body is parsed XML as JavaScript object
  return { data: request.body };
});
```

## Multipart Form Data

Use @fastify/multipart for file uploads. **Configure these critical options:**

```typescript
import fastifyMultipart from '@fastify/multipart';

app.register(fastifyMultipart, {
  // CRITICAL: Always set explicit limits
  limits: {
    fieldNameSize: 100,      // Max field name size in bytes
    fieldSize: 1024 * 1024,  // Max field value size (1MB)
    fields: 10,              // Max number of non-file fields
    fileSize: 10 * 1024 * 1024, // Max file size (10MB)
    files: 5,                // Max number of files
    headerPairs: 2000,       // Max number of header pairs
    parts: 1000,             // Max number of parts (fields + files)
  },
  // IMPORTANT: Throw on limit exceeded (default is to truncate silently!)
  throwFileSizeLimit: true,
  // Attach all fields to request.body for easier access
  attachFieldsToBody: true,
  // Only accept specific file types (security!)
  // onFile: async (part) => {
  //   if (!['image/jpeg', 'image/png'].includes(part.mimetype)) {
  //     throw new Error('Invalid file type');
  //   }
  // },
});

// Handle file upload
app.post('/upload', async (request, reply) => {
  const data = await request.file();

  if (!data) {
    return reply.code(400).send({ error: 'No file uploaded' });
  }

  // data.file is a stream
  const buffer = await data.toBuffer();

  return {
    filename: data.filename,
    mimetype: data.mimetype,
    size: buffer.length,
  };
});

// Handle multiple files
app.post('/upload-multiple', async (request) => {
  const files = [];

  for await (const part of request.files()) {
    const buffer = await part.toBuffer();
    files.push({
      filename: part.filename,
      mimetype: part.mimetype,
      size: buffer.length,
    });
  }

  return { files };
});

// Handle mixed form data
app.post('/form', async (request) => {
  const parts = request.parts();
  const fields: Record<string, string> = {};
  const files: Array<{ name: string; size: number }> = [];

  for await (const part of parts) {
    if (part.type === 'file') {
      const buffer = await part.toBuffer();
      files.push({ name: part.filename, size: buffer.length });
    } else {
      fields[part.fieldname] = part.value as string;
    }
  }

  return { fields, files };
});
```

## Stream Processing

Process body as stream for large payloads:

```typescript
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';

// Add parser that returns stream
app.addContentTypeParser(
  'application/octet-stream',
  async (request, payload) => {
    return payload; // Return stream directly
  },
);

app.post('/upload-stream', async (request, reply) => {
  const destination = createWriteStream('./upload.bin');

  await pipeline(request.body, destination);

  return { success: true };
});
```

## Custom JSON Parser

Replace the default JSON parser:

```typescript
// Remove default parser
app.removeContentTypeParser('application/json');

// Add custom parser with error handling
app.addContentTypeParser(
  'application/json',
  { parseAs: 'string' },
  async (request, body) => {
    try {
      return JSON.parse(body);
    } catch (error) {
      throw {
        statusCode: 400,
        code: 'INVALID_JSON',
        message: 'Invalid JSON payload',
      };
    }
  },
);
```

## Content Type with Parameters

Handle content types with parameters:

```typescript
// Match content type with any charset
app.addContentTypeParser(
  'application/json; charset=utf-8',
  { parseAs: 'string' },
  async (request, body) => {
    return JSON.parse(body);
  },
);

// Use regex for flexible matching
app.addContentTypeParser(
  /^application\/.*\+json$/,
  { parseAs: 'string' },
  async (request, body) => {
    return JSON.parse(body);
  },
);
```

## Catch-All Parser

Handle unknown content types:

```typescript
app.addContentTypeParser('*', async (request, payload) => {
  const chunks: Buffer[] = [];

  for await (const chunk of payload) {
    chunks.push(chunk);
  }

  const buffer = Buffer.concat(chunks);

  // Try to determine content type
  const contentType = request.headers['content-type'];

  if (contentType?.includes('json')) {
    return JSON.parse(buffer.toString('utf-8'));
  }

  if (contentType?.includes('text')) {
    return buffer.toString('utf-8');
  }

  return buffer;
});
```

## Body Limit Configuration

Configure body size limits:

```typescript
// Global limit
const app = Fastify({
  bodyLimit: 1048576, // 1MB
});

// Per-route limit
app.post('/large-upload', {
  bodyLimit: 52428800, // 50MB for this route
}, async (request) => {
  return { size: JSON.stringify(request.body).length };
});

// Per content type limit
app.addContentTypeParser('application/json', {
  parseAs: 'string',
  bodyLimit: 2097152, // 2MB for JSON
}, async (request, body) => {
  return JSON.parse(body);
});
```

## Protocol Buffers

Parse protobuf content:

```typescript
import protobuf from 'protobufjs';

const root = await protobuf.load('./schema.proto');
const MessageType = root.lookupType('package.MessageType');

app.addContentTypeParser(
  'application/x-protobuf',
  { parseAs: 'buffer' },
  async (request, body) => {
    const message = MessageType.decode(body);
    return MessageType.toObject(message);
  },
);
```

## Form Data with @fastify/formbody

Simple form parsing:

```typescript
import formbody from '@fastify/formbody';

app.register(formbody);

app.post('/form', async (request) => {
  // request.body is parsed form data
  const { name, email } = request.body as { name: string; email: string };
  return { name, email };
});
```

## Content Negotiation

Handle different request formats:

```typescript
app.post('/data', async (request, reply) => {
  const contentType = request.headers['content-type'];

  // Body is already parsed by the appropriate parser
  const data = request.body;

  // Respond based on Accept header
  const accept = request.headers.accept;

  if (accept?.includes('application/xml')) {
    reply.type('application/xml');
    return `<data>${JSON.stringify(data)}</data>`;
  }

  reply.type('application/json');
  return data;
});
```

## Validation After Parsing

Validate parsed content:

```typescript
app.post('/users', {
  schema: {
    body: {
      type: 'object',
      properties: {
        name: { type: 'string', minLength: 1 },
        email: { type: 'string', format: 'email' },
      },
      required: ['name', 'email'],
    },
  },
}, async (request) => {
  // Body is parsed AND validated
  return request.body;
});
```
rules/cors-security.md
---
name: cors-security
description: CORS and security headers in Fastify
metadata:
  tags: cors, security, headers, helmet, csrf
---

# CORS and Security

## Contents

- [CORS with @fastify/cors](#cors-with-fastifycors)
- [Dynamic CORS Origin](#dynamic-cors-origin)
- [Per-Route CORS](#per-route-cors)
- [Security Headers with @fastify/helmet](#security-headers-with-fastifyhelmet)
- [Configure Individual Headers](#configure-individual-headers)
- [Rate Limiting](#rate-limiting)
- [Redis-Based Rate Limiting](#redis-based-rate-limiting)
- [CSRF Protection](#csrf-protection)
- [Custom Security Headers](#custom-security-headers)
- [Secure Cookies](#secure-cookies)
- [Request Validation Security](#request-validation-security)
- [IP Filtering](#ip-filtering)
- [Trust Proxy](#trust-proxy)
- [HTTPS Redirect](#https-redirect)
- [Security Best Practices Summary](#security-best-practices-summary)

## CORS with @fastify/cors

Enable Cross-Origin Resource Sharing:

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

## Dynamic CORS Origin

Validate origins dynamically:

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

## Per-Route CORS

Configure CORS for specific routes:

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

## Security Headers with @fastify/helmet

Add security headers:

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

## Configure Individual Headers

Fine-tune security headers:

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

## Rate Limiting

Protect against abuse:

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

## Redis-Based Rate Limiting

Use Redis for distributed rate limiting:

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

## CSRF Protection

Protect against Cross-Site Request Forgery:

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

## Custom Security Headers

Add custom headers:

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

## Secure Cookies

Configure secure cookies:

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

## Request Validation Security

Validate and sanitize input:

```typescript
// 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);
```

## IP Filtering

Restrict access by IP:

```typescript
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;
}
```

## Trust Proxy

Configure for reverse proxy environments:

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

## HTTPS Redirect

Force HTTPS in production:

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

## Security Best Practices Summary

```typescript
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
```
rules/database.md
---
name: database
description: Database integration with Fastify using official adapters
metadata:
  tags: database, postgres, mysql, mongodb, redis, sql
---

# Database Integration

## Contents

- [Use Official Fastify Database Adapters](#use-official-fastify-database-adapters)
- [PostgreSQL with @fastify/postgres](#postgresql-with-fastifypostgres)
- [MySQL with @fastify/mysql](#mysql-with-fastifymysql)
- [MongoDB with @fastify/mongodb](#mongodb-with-fastifymongodb)
- [Redis with @fastify/redis](#redis-with-fastifyredis)
- [Database as Plugin](#database-as-plugin)
- [Repository Pattern](#repository-pattern)
- [Testing with Database](#testing-with-database)
- [Connection Pool Configuration](#connection-pool-configuration)

## Use Official Fastify Database Adapters

Always use the official Fastify database plugins from the `@fastify` organization. They provide proper connection pooling, encapsulation, and integration with Fastify's lifecycle.

## PostgreSQL with @fastify/postgres

```typescript
import Fastify from 'fastify';
import fastifyPostgres from '@fastify/postgres';

const app = Fastify({ logger: true });

app.register(fastifyPostgres, {
  connectionString: process.env.DATABASE_URL,
});

// Use in routes
app.get('/users', async (request) => {
  const client = await app.pg.connect();
  try {
    const { rows } = await client.query('SELECT * FROM users');
    return rows;
  } finally {
    client.release();
  }
});

// Or use the pool directly for simple queries
app.get('/users/:id', async (request) => {
  const { id } = request.params;
  const { rows } = await app.pg.query(
    'SELECT * FROM users WHERE id = $1',
    [id],
  );
  return rows[0];
});

// Transactions
app.post('/transfer', async (request) => {
  const { fromId, toId, amount } = request.body;
  const client = await app.pg.connect();

  try {
    await client.query('BEGIN');
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
      [amount, fromId],
    );
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount, toId],
    );
    await client.query('COMMIT');
    return { success: true };
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
});
```

## MySQL with @fastify/mysql

```typescript
import Fastify from 'fastify';
import fastifyMysql from '@fastify/mysql';

const app = Fastify({ logger: true });

app.register(fastifyMysql, {
  promise: true,
  connectionString: process.env.MYSQL_URL,
});

app.get('/users', async (request) => {
  const connection = await app.mysql.getConnection();
  try {
    const [rows] = await connection.query('SELECT * FROM users');
    return rows;
  } finally {
    connection.release();
  }
});
```

## MongoDB with @fastify/mongodb

```typescript
import Fastify from 'fastify';
import fastifyMongo from '@fastify/mongodb';

const app = Fastify({ logger: true });

app.register(fastifyMongo, {
  url: process.env.MONGODB_URL,
});

app.get('/users', async (request) => {
  const users = await app.mongo.db
    .collection('users')
    .find({})
    .toArray();
  return users;
});

app.get('/users/:id', async (request) => {
  const { id } = request.params;
  const user = await app.mongo.db
    .collection('users')
    .findOne({ _id: new app.mongo.ObjectId(id) });
  return user;
});

app.post('/users', async (request) => {
  const result = await app.mongo.db
    .collection('users')
    .insertOne(request.body);
  return { id: result.insertedId };
});
```

## Redis with @fastify/redis

```typescript
import Fastify from 'fastify';
import fastifyRedis from '@fastify/redis';

const app = Fastify({ logger: true });

app.register(fastifyRedis, {
  url: process.env.REDIS_URL,
});

// Caching example
app.get('/data/:key', async (request) => {
  const { key } = request.params;

  // Try cache first
  const cached = await app.redis.get(`cache:${key}`);
  if (cached) {
    return JSON.parse(cached);
  }

  // Fetch from database
  const data = await fetchFromDatabase(key);

  // Cache for 5 minutes
  await app.redis.setex(`cache:${key}`, 300, JSON.stringify(data));

  return data;
});
```

## Database as Plugin

Encapsulate database access in a plugin:

```typescript
// plugins/database.ts
import fp from 'fastify-plugin';
import fastifyPostgres from '@fastify/postgres';

export default fp(async function databasePlugin(fastify) {
  await fastify.register(fastifyPostgres, {
    connectionString: fastify.config.DATABASE_URL,
  });

  // Add health check
  fastify.decorate('checkDatabaseHealth', async () => {
    try {
      await fastify.pg.query('SELECT 1');
      return true;
    } catch {
      return false;
    }
  });
}, {
  name: 'database',
  dependencies: ['config'],
});
```

## Repository Pattern

Abstract database access with repositories:

```typescript
// repositories/user.repository.ts
import type { FastifyInstance } from 'fastify';

export interface User {
  id: string;
  email: string;
  name: string;
}

export function createUserRepository(app: FastifyInstance) {
  return {
    async findById(id: string): Promise<User | null> {
      const { rows } = await app.pg.query(
        'SELECT * FROM users WHERE id = $1',
        [id],
      );
      return rows[0] || null;
    },

    async findByEmail(email: string): Promise<User | null> {
      const { rows } = await app.pg.query(
        'SELECT * FROM users WHERE email = $1',
        [email],
      );
      return rows[0] || null;
    },

    async create(data: Omit<User, 'id'>): Promise<User> {
      const { rows } = await app.pg.query(
        'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
        [data.email, data.name],
      );
      return rows[0];
    },

    async update(id: string, data: Partial<User>): Promise<User | null> {
      const fields = Object.keys(data);
      const values = Object.values(data);
      const setClause = fields
        .map((f, i) => `${f} = $${i + 2}`)
        .join(', ');

      const { rows } = await app.pg.query(
        `UPDATE users SET ${setClause} WHERE id = $1 RETURNING *`,
        [id, ...values],
      );
      return rows[0] || null;
    },

    async delete(id: string): Promise<boolean> {
      const { rowCount } = await app.pg.query(
        'DELETE FROM users WHERE id = $1',
        [id],
      );
      return rowCount > 0;
    },
  };
}

// Usage in plugin
import fp from 'fastify-plugin';
import { createUserRepository } from './repositories/user.repository.js';

export default fp(async function repositoriesPlugin(fastify) {
  fastify.decorate('repositories', {
    users: createUserRepository(fastify),
  });
}, {
  name: 'repositories',
  dependencies: ['database'],
});
```

## Testing with Database

Use transactions for test isolation:

```typescript
import { describe, it, beforeEach, afterEach } from 'node:test';
import { build } from './app.js';

describe('User API', () => {
  let app;
  let client;

  beforeEach(async () => {
    app = await build();
    client = await app.pg.connect();
    await client.query('BEGIN');
  });

  afterEach(async () => {
    await client.query('ROLLBACK');
    client.release();
    await app.close();
  });

  it('should create a user', async (t) => {
    const response = await app.inject({
      method: 'POST',
      url: '/users',
      payload: { email: '[email protected]', name: 'Test' },
    });

    t.assert.equal(response.statusCode, 201);
  });
});
```

## Connection Pool Configuration

Configure connection pools appropriately:

```typescript
app.register(fastifyPostgres, {
  connectionString: process.env.DATABASE_URL,
  // Pool configuration
  max: 20,                    // Maximum pool size
  idleTimeoutMillis: 30000,   // Close idle clients after 30s
  connectionTimeoutMillis: 5000, // Timeout for new connections
});
```
rules/decorators.md
---
name: decorators
description: Decorators and request/reply extensions in Fastify
metadata:
  tags: decorators, extensions, customization, utilities
---

# Decorators and Extensions

## Contents

- [Understanding Decorators](#understanding-decorators)
- [Decorator Types](#decorator-types)
- [TypeScript Declaration Merging](#typescript-declaration-merging)
- [Decorator Initialization](#decorator-initialization)
- [Dependency Injection with Decorators](#dependency-injection-with-decorators)
- [Request Context Pattern](#request-context-pattern)
- [Reply Helpers](#reply-helpers)
- [Checking Decorators](#checking-decorators)
- [Decorator Encapsulation](#decorator-encapsulation)
- [Functional Decorators](#functional-decorators)
- [Async Decorator Initialization](#async-decorator-initialization)

## Understanding Decorators

Decorators add custom properties and methods to Fastify instances, requests, and replies:

```typescript
import Fastify from 'fastify';

const app = Fastify();

// Decorate the Fastify instance
app.decorate('utility', {
  formatDate: (date: Date) => date.toISOString(),
  generateId: () => crypto.randomUUID(),
});

// Use in routes
app.get('/example', async function (request, reply) {
  const id = this.utility.generateId();
  return { id, timestamp: this.utility.formatDate(new Date()) };
});
```

## Decorator Types

Three types of decorators for different contexts:

```typescript
// Instance decorator - available on fastify instance
app.decorate('config', { apiVersion: '1.0.0' });
app.decorate('db', databaseConnection);
app.decorate('cache', cacheClient);

// Request decorator - available on each request
app.decorateRequest('user', null);           // Object property
app.decorateRequest('startTime', 0);         // Primitive
app.decorateRequest('getData', function() {  // Method
  return this.body;
});

// Reply decorator - available on each reply
app.decorateReply('sendError', function(code: number, message: string) {
  return this.code(code).send({ error: message });
});
app.decorateReply('success', function(data: unknown) {
  return this.send({ success: true, data });
});
```

## TypeScript Declaration Merging

Extend Fastify types for type safety:

```typescript
// Declare custom properties
declare module 'fastify' {
  interface FastifyInstance {
    config: {
      apiVersion: string;
      environment: string;
    };
    db: DatabaseClient;
    cache: CacheClient;
  }

  interface FastifyRequest {
    user: {
      id: string;
      email: string;
      roles: string[];
    } | null;
    startTime: number;
    requestId: string;
  }

  interface FastifyReply {
    sendError: (code: number, message: string) => void;
    success: (data: unknown) => void;
  }
}

// Register decorators
app.decorate('config', {
  apiVersion: '1.0.0',
  environment: process.env.NODE_ENV,
});

app.decorateRequest('user', null);
app.decorateRequest('startTime', 0);

app.decorateReply('sendError', function (code: number, message: string) {
  this.code(code).send({ error: message });
});
```

## Decorator Initialization

Initialize request/reply decorators in hooks:

```typescript
// Decorators with primitive defaults are copied
app.decorateRequest('startTime', 0);

// Initialize in hook
app.addHook('onRequest', async (request) => {
  request.startTime = Date.now();
});

// Object decorators need getter pattern for proper initialization
app.decorateRequest('context', null);

app.addHook('onRequest', async (request) => {
  request.context = {
    traceId: request.headers['x-trace-id'] || crypto.randomUUID(),
    clientIp: request.ip,
    userAgent: request.headers['user-agent'],
  };
});
```

## Dependency Injection with Decorators

Use decorators for dependency injection:

```typescript
import fp from 'fastify-plugin';

// Database plugin
export default fp(async function databasePlugin(fastify, options) {
  const db = await createDatabaseConnection(options.connectionString);

  fastify.decorate('db', db);

  fastify.addHook('onClose', async () => {
    await db.close();
  });
});

// User service plugin
export default fp(async function userServicePlugin(fastify) {
  // Depends on db decorator
  if (!fastify.hasDecorator('db')) {
    throw new Error('Database plugin must be registered first');
  }

  const userService = {
    findById: (id: string) => fastify.db.query('SELECT * FROM users WHERE id = $1', [id]),
    create: (data: CreateUserInput) => fastify.db.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [data.name, data.email]
    ),
  };

  fastify.decorate('userService', userService);
}, {
  dependencies: ['database-plugin'],
});

// Use in routes
app.get('/users/:id', async function (request) {
  const user = await this.userService.findById(request.params.id);
  return user;
});
```

## Request Context Pattern

Build rich request context:

```typescript
interface RequestContext {
  traceId: string;
  user: User | null;
  permissions: Set<string>;
  startTime: number;
  metadata: Map<string, unknown>;
}

declare module 'fastify' {
  interface FastifyRequest {
    ctx: RequestContext;
  }
}

app.decorateRequest('ctx', null);

app.addHook('onRequest', async (request) => {
  request.ctx = {
    traceId: request.headers['x-trace-id']?.toString() || crypto.randomUUID(),
    user: null,
    permissions: new Set(),
    startTime: Date.now(),
    metadata: new Map(),
  };
});

// Auth hook populates user
app.addHook('preHandler', async (request) => {
  const token = request.headers.authorization;
  if (token) {
    const user = await verifyToken(token);
    request.ctx.user = user;
    request.ctx.permissions = new Set(user.permissions);
  }
});

// Use in handlers
app.get('/profile', async (request, reply) => {
  if (!request.ctx.user) {
    return reply.code(401).send({ error: 'Unauthorized' });
  }

  if (!request.ctx.permissions.has('read:profile')) {
    return reply.code(403).send({ error: 'Forbidden' });
  }

  return request.ctx.user;
});
```

## Reply Helpers

Create consistent response methods:

```typescript
declare module 'fastify' {
  interface FastifyReply {
    ok: (data?: unknown) => void;
    created: (data: unknown) => void;
    noContent: () => void;
    badRequest: (message: string, details?: unknown) => void;
    unauthorized: (message?: string) => void;
    forbidden: (message?: string) => void;
    notFound: (resource?: string) => void;
    conflict: (message: string) => void;
    serverError: (message?: string) => void;
  }
}

app.decorateReply('ok', function (data?: unknown) {
  this.code(200).send(data ?? { success: true });
});

app.decorateReply('created', function (data: unknown) {
  this.code(201).send(data);
});

app.decorateReply('noContent', function () {
  this.code(204).send();
});

app.decorateReply('badRequest', function (message: string, details?: unknown) {
  this.code(400).send({
    statusCode: 400,
    error: 'Bad Request',
    message,
    details,
  });
});

app.decorateReply('unauthorized', function (message = 'Authentication required') {
  this.code(401).send({
    statusCode: 401,
    error: 'Unauthorized',
    message,
  });
});

app.decorateReply('notFound', function (resource = 'Resource') {
  this.code(404).send({
    statusCode: 404,
    error: 'Not Found',
    message: `${resource} not found`,
  });
});

// Usage
app.get('/users/:id', async (request, reply) => {
  const user = await db.users.findById(request.params.id);
  if (!user) {
    return reply.notFound('User');
  }
  return reply.ok(user);
});

app.post('/users', async (request, reply) => {
  const user = await db.users.create(request.body);
  return reply.created(user);
});
```

## Checking Decorators

Check if decorators exist before using:

```typescript
// Check at registration time
app.register(async function (fastify) {
  if (!fastify.hasDecorator('db')) {
    throw new Error('Database decorator required');
  }

  if (!fastify.hasRequestDecorator('user')) {
    throw new Error('User request decorator required');
  }

  if (!fastify.hasReplyDecorator('sendError')) {
    throw new Error('sendError reply decorator required');
  }

  // Safe to use decorators
});
```

## Decorator Encapsulation

Decorators respect encapsulation by default:

```typescript
app.register(async function pluginA(fastify) {
  fastify.decorate('pluginAUtil', () => 'A');

  fastify.get('/a', async function () {
    return this.pluginAUtil(); // Works
  });
});

app.register(async function pluginB(fastify) {
  // this.pluginAUtil is NOT available here (encapsulated)

  fastify.get('/b', async function () {
    // this.pluginAUtil() would be undefined
  });
});
```

Use `fastify-plugin` to share decorators:

```typescript
import fp from 'fastify-plugin';

export default fp(async function sharedDecorator(fastify) {
  fastify.decorate('sharedUtil', () => 'shared');
});

// Now available to parent and sibling plugins
```

## Functional Decorators

Create decorators that return functions:

```typescript
declare module 'fastify' {
  interface FastifyInstance {
    createValidator: <T>(schema: object) => (data: unknown) => T;
    createRateLimiter: (options: RateLimitOptions) => RateLimiter;
  }
}

app.decorate('createValidator', function <T>(schema: object) {
  const validate = ajv.compile(schema);
  return (data: unknown): T => {
    if (!validate(data)) {
      throw new ValidationError(validate.errors);
    }
    return data as T;
  };
});

// Usage
const validateUser = app.createValidator<User>(userSchema);

app.post('/users', async (request) => {
  const user = validateUser(request.body);
  return db.users.create(user);
});
```

## Async Decorator Initialization

Handle async initialization properly:

```typescript
import fp from 'fastify-plugin';

export default fp(async function asyncPlugin(fastify) {
  // Async initialization
  const connection = await createAsyncConnection();
  const cache = await initializeCache();

  fastify.decorate('asyncService', {
    connection,
    cache,
    query: async (sql: string) => connection.query(sql),
  });

  fastify.addHook('onClose', async () => {
    await connection.close();
    await cache.disconnect();
  });
});

// Plugin is fully initialized before routes execute
app.get('/data', async function () {
  return this.asyncService.query('SELECT * FROM data');
});
```
rules/deployment.md
---
name: deployment
description: Production deployment for Fastify applications
metadata:
  tags: deployment, production, docker, kubernetes, scaling
---

# Production Deployment

## Contents

- [Graceful Shutdown with close-with-grace](#graceful-shutdown-with-close-with-grace)
- [Health Check Endpoints](#health-check-endpoints)
- [Docker Configuration](#docker-configuration)
- [Kubernetes Deployment](#kubernetes-deployment)
- [Production Logger Configuration](#production-logger-configuration)
- [Request Timeouts](#request-timeouts)
- [Trust Proxy Settings](#trust-proxy-settings)
- [Static File Serving](#static-file-serving)
- [Compression](#compression)
- [Metrics and Monitoring](#metrics-and-monitoring)
- [Zero-Downtime Deployments](#zero-downtime-deployments)

## Graceful Shutdown with close-with-grace

Use `close-with-grace` for proper shutdown handling:

```typescript
import Fastify from 'fastify';
import closeWithGrace from 'close-with-grace';

const app = Fastify({ logger: true });

// Register plugins and routes
await app.register(import('./plugins/index.js'));
await app.register(import('./routes/index.js'));

// Graceful shutdown handler
closeWithGrace({ delay: 10000 }, async ({ signal, err }) => {
  if (err) {
    app.log.error({ err }, 'Server closing due to error');
  } else {
    app.log.info({ signal }, 'Server closing due to signal');
  }

  await app.close();
});

// Start server
await app.listen({
  port: parseInt(process.env.PORT || '3000', 10),
  host: '0.0.0.0',
});

app.log.info(`Server listening on ${app.server.address()}`);
```

## Health Check Endpoints

Implement comprehensive health checks:

```typescript
app.get('/health', async () => {
  return { status: 'ok', timestamp: new Date().toISOString() };
});

app.get('/health/live', async () => {
  return { status: 'ok' };
});

app.get('/health/ready', async (request, reply) => {
  const checks = {
    database: false,
    cache: false,
  };

  try {
    await app.db`SELECT 1`;
    checks.database = true;
  } catch {
    // Database not ready
  }

  try {
    await app.cache.ping();
    checks.cache = true;
  } catch {
    // Cache not ready
  }

  const allHealthy = Object.values(checks).every(Boolean);

  if (!allHealthy) {
    reply.code(503);
  }

  return {
    status: allHealthy ? 'ok' : 'degraded',
    checks,
    timestamp: new Date().toISOString(),
  };
});

// Detailed health for monitoring
app.get('/health/details', {
  preHandler: [app.authenticate, app.requireAdmin],
}, async () => {
  const memory = process.memoryUsage();

  return {
    status: 'ok',
    uptime: process.uptime(),
    memory: {
      heapUsed: Math.round(memory.heapUsed / 1024 / 1024),
      heapTotal: Math.round(memory.heapTotal / 1024 / 1024),
      rss: Math.round(memory.rss / 1024 / 1024),
    },
    version: process.env.APP_VERSION,
    nodeVersion: process.version,
  };
});
```

## Docker Configuration

Create an optimized Dockerfile:

```dockerfile
# Build stage
FROM node:22-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Production stage
FROM node:22-alpine

WORKDIR /app

# Run as non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Copy from builder
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/src ./src
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

EXPOSE 3000

ENV NODE_ENV=production
ENV PORT=3000

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "src/app.ts"]
```

```yaml
# docker-compose.yml
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:pass@db:5432/app
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:
```

## Kubernetes Deployment

Deploy to Kubernetes:

```yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastify-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: fastify-api
  template:
    metadata:
      labels:
        app: fastify-api
    spec:
      containers:
        - name: api
          image: my-registry/fastify-api:latest
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: api-secrets
                  key: database-url
          resources:
            requests:
              memory: "256Mi"
              cpu: "100m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health/live
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]
---
apiVersion: v1
kind: Service
metadata:
  name: fastify-api
spec:
  selector:
    app: fastify-api
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP
```

## Production Logger Configuration

Configure logging for production:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  logger: {
    level: process.env.LOG_LEVEL || 'info',
    // JSON output for log aggregation
    formatters: {
      level: (label) => ({ level: label }),
      bindings: (bindings) => ({
        pid: bindings.pid,
        hostname: bindings.hostname,
        service: 'fastify-api',
        version: process.env.APP_VERSION,
      }),
    },
    timestamp: () => `,"time":"${new Date().toISOString()}"`,
    // Redact sensitive data
    redact: {
      paths: [
        'req.headers.authorization',
        'req.headers.cookie',
        '*.password',
        '*.token',
        '*.secret',
      ],
      censor: '[REDACTED]',
    },
  },
});
```

## Request Timeouts

Configure appropriate timeouts:

```typescript
const app = Fastify({
  connectionTimeout: 30000,     // 30s connection timeout
  keepAliveTimeout: 72000,      // 72s keep-alive (longer than ALB 60s)
  requestTimeout: 30000,        // 30s request timeout
  bodyLimit: 1048576,           // 1MB body limit
});

// Per-route timeout
app.get('/long-operation', {
  config: {
    timeout: 60000, // 60s for this route
  },
}, longOperationHandler);
```

## Trust Proxy Settings

Configure for load balancers:

```typescript
const app = Fastify({
  // Trust first proxy (load balancer)
  trustProxy: true,

  // Or trust specific proxies
  trustProxy: ['127.0.0.1', '10.0.0.0/8'],

  // Or number of proxies to trust
  trustProxy: 1,
});

// Now request.ip returns real client IP
```

## Static File Serving

Serve static files efficiently. **Always use `import.meta.dirname` as the base path**, never `process.cwd()`:

```typescript
import fastifyStatic from '@fastify/static';
import { join } from 'node:path';

app.register(fastifyStatic, {
  root: join(import.meta.dirname, '..', 'public'),
  prefix: '/static/',
  maxAge: '1d',
  immutable: true,
  etag: true,
  lastModified: true,
});
```

## Compression

Enable response compression:

```typescript
import fastifyCompress from '@fastify/compress';

app.register(fastifyCompress, {
  global: true,
  threshold: 1024, // Only compress > 1KB
  encodings: ['gzip', 'deflate'],
});
```

## Metrics and Monitoring

Expose Prometheus metrics:

```typescript
import { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client';

collectDefaultMetrics();

const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
});

const httpRequestTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status'],
});

app.addHook('onResponse', (request, reply, done) => {
  const route = request.routeOptions.url || request.url;
  const labels = {
    method: request.method,
    route,
    status: reply.statusCode,
  };

  httpRequestDuration.observe(labels, reply.elapsedTime / 1000);
  httpRequestTotal.inc(labels);
  done();
});

app.get('/metrics', async (request, reply) => {
  reply.header('Content-Type', register.contentType);
  return register.metrics();
});
```

## Zero-Downtime Deployments

Support rolling updates:

```typescript
import closeWithGrace from 'close-with-grace';

// Stop accepting new connections gracefully
closeWithGrace({ delay: 30000 }, async ({ signal }) => {
  app.log.info({ signal }, 'Received shutdown signal');

  // Stop accepting new connections
  // Existing connections continue to be served

  // Wait for in-flight requests (handled by close-with-grace delay)
  await app.close();

  app.log.info('Server closed');
});
```

rules/error-handling.md
---
name: error-handling
description: Error handling patterns in Fastify
metadata:
  tags: errors, exceptions, error-handler, validation
---

# Error Handling in Fastify

## Contents

- [Default Error Handler](#default-error-handler)
- [Custom Error Classes](#custom-error-classes)
- [Custom Error Handler](#custom-error-handler)
- [Error Response Schema](#error-response-schema)
- [Reply Helpers with @fastify/sensible](#reply-helpers-with-fastifysensible)
- [Async Error Handling](#async-error-handling)
- [Hook Error Handling](#hook-error-handling)
- [Not Found Handler](#not-found-handler)
- [Error Wrapping](#error-wrapping)
- [Validation Error Customization](#validation-error-customization)
- [Error Cause Chain](#error-cause-chain)
- [Plugin-Scoped Error Handlers](#plugin-scoped-error-handlers)
- [Graceful Error Recovery](#graceful-error-recovery)

## Default Error Handler

Fastify has a built-in error handler. Thrown errors automatically become HTTP responses:

```typescript
import Fastify from 'fastify';

const app = Fastify({ logger: true });

app.get('/users/:id', async (request) => {
  const user = await findUser(request.params.id);
  if (!user) {
    // Throwing an error with statusCode sets the response status
    const error = new Error('User not found');
    error.statusCode = 404;
    throw error;
  }
  return user;
});
```

## Custom Error Classes

Use `@fastify/error` for creating typed errors:

```typescript
import createError from '@fastify/error';

const NotFoundError = createError('NOT_FOUND', '%s not found', 404);
const UnauthorizedError = createError('UNAUTHORIZED', 'Authentication required', 401);
const ForbiddenError = createError('FORBIDDEN', 'Access denied: %s', 403);
const ValidationError = createError('VALIDATION_ERROR', '%s', 400);
const ConflictError = createError('CONFLICT', '%s already exists', 409);

// Usage
app.get('/users/:id', async (request) => {
  const user = await findUser(request.params.id);
  if (!user) {
    throw new NotFoundError('User');
  }
  return user;
});

app.post('/users', async (request) => {
  const exists = await userExists(request.body.email);
  if (exists) {
    throw new ConflictError('Email');
  }
  return createUser(request.body);
});
```

## Custom Error Handler

Implement a centralized error handler:

```typescript
import Fastify from 'fastify';
import type { FastifyError, FastifyRequest, FastifyReply } from 'fastify';

const app = Fastify({ logger: true });

app.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
  // Log the error
  request.log.error({ err: error }, 'Request error');

  // Handle validation errors
  if (error.validation) {
    return reply.code(400).send({
      statusCode: 400,
      error: 'Bad Request',
      message: 'Validation failed',
      details: error.validation,
    });
  }

  // Handle known errors with status codes
  const statusCode = error.statusCode ?? 500;
  const code = error.code ?? 'INTERNAL_ERROR';

  // Don't expose internal error details in production
  const message = statusCode >= 500 && process.env.NODE_ENV === 'production'
    ? 'Internal Server Error'
    : error.message;

  return reply.code(statusCode).send({
    statusCode,
    error: code,
    message,
  });
});
```

## Error Response Schema

Define consistent error response schemas:

```typescript
app.addSchema({
  $id: 'httpError',
  type: 'object',
  properties: {
    statusCode: { type: 'integer' },
    error: { type: 'string' },
    message: { type: 'string' },
    details: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          field: { type: 'string' },
          message: { type: 'string' },
        },
      },
    },
  },
  required: ['statusCode', 'error', 'message'],
});

// Use in route schemas
app.get('/users/:id', {
  schema: {
    params: {
      type: 'object',
      properties: { id: { type: 'string' } },
      required: ['id'],
    },
    response: {
      200: { $ref: 'user#' },
      404: { $ref: 'httpError#' },
      500: { $ref: 'httpError#' },
    },
  },
}, handler);
```

## Reply Helpers with @fastify/sensible

Use `@fastify/sensible` for standard HTTP errors:

```typescript
import fastifySensible from '@fastify/sensible';

app.register(fastifySensible);

app.get('/users/:id', async (request, reply) => {
  const user = await findUser(request.params.id);
  if (!user) {
    return reply.notFound('User not found');
  }
  if (!hasAccess(request.user, user)) {
    return reply.forbidden('You cannot access this user');
  }
  return user;
});

// Available methods:
// reply.badRequest(message?)
// reply.unauthorized(message?)
// reply.forbidden(message?)
// reply.notFound(message?)
// reply.methodNotAllowed(message?)
// reply.conflict(message?)
// reply.gone(message?)
// reply.unprocessableEntity(message?)
// reply.tooManyRequests(message?)
// reply.internalServerError(message?)
// reply.notImplemented(message?)
// reply.badGateway(message?)
// reply.serviceUnavailable(message?)
// reply.gatewayTimeout(message?)
```

## Async Error Handling

Errors in async handlers are automatically caught:

```typescript
// Errors are automatically caught and passed to error handler
app.get('/users', async (request) => {
  const users = await db.users.findAll(); // If this throws, error handler catches it
  return users;
});

// Explicit error handling for custom logic
app.get('/users/:id', async (request, reply) => {
  try {
    const user = await db.users.findById(request.params.id);
    if (!user) {
      return reply.code(404).send({ error: 'User not found' });
    }
    return user;
  } catch (error) {
    // Transform database errors
    if (error.code === 'CONNECTION_ERROR') {
      request.log.error({ err: error }, 'Database connection failed');
      return reply.code(503).send({ error: 'Service temporarily unavailable' });
    }
    throw error; // Re-throw for error handler
  }
});
```

## Hook Error Handling

Errors in hooks are handled the same way:

```typescript
app.addHook('onRequest', async (request, reply) => {
  const token = request.headers.authorization;
  if (!token) {
    // This error goes to the error handler
    throw new UnauthorizedError();
  }

  try {
    request.user = await verifyToken(token);
  } catch (error) {
    throw new UnauthorizedError();
  }
});

// Or use reply to send response directly
app.addHook('onRequest', async (request, reply) => {
  if (!request.headers.authorization) {
    reply.code(401).send({ error: 'Unauthorized' });
    return; // Must return to stop processing
  }
});
```

## Not Found Handler

Customize the 404 response:

```typescript
app.setNotFoundHandler(async (request, reply) => {
  return reply.code(404).send({
    statusCode: 404,
    error: 'Not Found',
    message: `Route ${request.method} ${request.url} not found`,
  });
});

// With schema validation
app.setNotFoundHandler({
  preValidation: async (request, reply) => {
    // Pre-validation hook for 404 handler
  },
}, async (request, reply) => {
  return reply.code(404).send({ error: 'Not Found' });
});
```

## Error Wrapping

Wrap external errors with context:

```typescript
import createError from '@fastify/error';

const DatabaseError = createError('DATABASE_ERROR', 'Database operation failed: %s', 500);
const ExternalServiceError = createError('EXTERNAL_SERVICE_ERROR', 'External service failed: %s', 502);

app.get('/users/:id', async (request) => {
  try {
    return await db.users.findById(request.params.id);
  } catch (error) {
    throw new DatabaseError(error.message, { cause: error });
  }
});

app.get('/weather', async (request) => {
  try {
    return await weatherApi.fetch(request.query.city);
  } catch (error) {
    throw new ExternalServiceError(error.message, { cause: error });
  }
});
```

## Validation Error Customization

Customize validation error format:

```typescript
app.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    const details = error.validation.map((err) => {
      const field = err.instancePath
        ? err.instancePath.slice(1).replace(/\//g, '.')
        : err.params?.missingProperty || 'unknown';

      return {
        field,
        message: err.message,
        value: err.data,
      };
    });

    return reply.code(400).send({
      statusCode: 400,
      error: 'Validation Error',
      message: `Invalid ${error.validationContext}: ${details.map(d => d.field).join(', ')}`,
      details,
    });
  }

  // Handle other errors...
  throw error;
});
```

## Error Cause Chain

Preserve error chains for debugging:

```typescript
app.get('/complex-operation', async (request) => {
  try {
    await step1();
  } catch (error) {
    const wrapped = new Error('Step 1 failed', { cause: error });
    wrapped.statusCode = 500;
    throw wrapped;
  }
});

// In error handler, log the full chain
app.setErrorHandler((error, request, reply) => {
  // Log error with cause chain
  let current = error;
  const chain = [];
  while (current) {
    chain.push({
      message: current.message,
      code: current.code,
      stack: current.stack,
    });
    current = current.cause;
  }

  request.log.error({ errorChain: chain }, 'Request failed');

  reply.code(error.statusCode || 500).send({
    error: error.message,
  });
});
```

## Plugin-Scoped Error Handlers

Set error handlers at the plugin level:

```typescript
app.register(async function apiRoutes(fastify) {
  // This error handler only applies to routes in this plugin
  fastify.setErrorHandler((error, request, reply) => {
    request.log.error({ err: error }, 'API error');

    reply.code(error.statusCode || 500).send({
      error: {
        code: error.code || 'API_ERROR',
        message: error.message,
      },
    });
  });

  fastify.get('/data', async () => {
    throw new Error('API-specific error');
  });
}, { prefix: '/api' });
```

## Graceful Error Recovery

Handle errors gracefully without crashing:

```typescript
app.get('/resilient', async (request, reply) => {
  const results = await Promise.allSettled([
    fetchPrimaryData(),
    fetchSecondaryData(),
    fetchOptionalData(),
  ]);

  const [primary, secondary, optional] = results;

  if (primary.status === 'rejected') {
    // Primary data is required
    throw new Error('Primary data unavailable');
  }

  return {
    data: primary.value,
    secondary: secondary.status === 'fulfilled' ? secondary.value : null,
    optional: optional.status === 'fulfilled' ? optional.value : null,
    warnings: results
      .filter((r) => r.status === 'rejected')
      .map((r) => r.reason.message),
  };
});
```
rules/hooks.md
---
name: hooks
description: Hooks and request lifecycle in Fastify
metadata:
  tags: hooks, lifecycle, middleware, onRequest, preHandler
---

# Hooks and Request Lifecycle

## Contents

- [Request Lifecycle Overview](#request-lifecycle-overview)
- [onRequest Hook](#onrequest-hook)
- [preParsing Hook](#preparsing-hook)
- [preValidation Hook](#prevalidation-hook)
- [preHandler Hook](#prehandler-hook)
- [preSerialization Hook](#preserialization-hook)
- [onSend Hook](#onsend-hook)
- [onResponse Hook](#onresponse-hook)
- [onError Hook](#onerror-hook)
- [onTimeout Hook](#ontimeout-hook)
- [onRequestAbort Hook](#onrequestabort-hook)
- [Application Lifecycle Hooks](#application-lifecycle-hooks)
- [Scoped Hooks](#scoped-hooks)
- [Hook Execution Order](#hook-execution-order)
- [Stopping Hook Execution](#stopping-hook-execution)
- [Route-Level Hooks](#route-level-hooks)
- [Async Hook Patterns](#async-hook-patterns)

## Request Lifecycle Overview

Fastify executes hooks in a specific order:

```
Incoming Request
       |
   onRequest
       |
   preParsing
       |
   preValidation
       |
   preHandler
       |
     Handler
       |
   preSerialization
       |
    onSend
       |
   onResponse
```

## onRequest Hook

First hook to execute, before body parsing. Use for authentication, request ID setup:

```typescript
import Fastify from 'fastify';

const app = Fastify();

// Global onRequest hook
app.addHook('onRequest', async (request, reply) => {
  request.startTime = Date.now();
  request.log.info({ url: request.url, method: request.method }, 'Request started');
});

// Authentication check
app.addHook('onRequest', async (request, reply) => {
  // Skip auth for public routes
  if (request.url.startsWith('/public')) {
    return;
  }

  const token = request.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    reply.code(401).send({ error: 'Unauthorized' });
    return; // Stop processing
  }

  try {
    request.user = await verifyToken(token);
  } catch {
    reply.code(401).send({ error: 'Invalid token' });
  }
});
```

## preParsing Hook

Execute before body parsing. Can modify the payload stream:

```typescript
app.addHook('preParsing', async (request, reply, payload) => {
  // Log raw payload size
  request.log.debug({ contentLength: request.headers['content-length'] }, 'Parsing body');

  // Return modified payload stream if needed
  return payload;
});

// Decompress incoming data
app.addHook('preParsing', async (request, reply, payload) => {
  if (request.headers['content-encoding'] === 'gzip') {
    return payload.pipe(zlib.createGunzip());
  }
  return payload;
});
```

## preValidation Hook

Execute after parsing, before schema validation:

```typescript
app.addHook('preValidation', async (request, reply) => {
  // Modify body before validation
  if (request.body && typeof request.body === 'object') {
    // Normalize data
    request.body.email = request.body.email?.toLowerCase().trim();
  }
});

// Rate limiting check
app.addHook('preValidation', async (request, reply) => {
  const key = request.ip;
  const count = await redis.incr(`ratelimit:${key}`);

  if (count === 1) {
    await redis.expire(`ratelimit:${key}`, 60);
  }

  if (count > 100) {
    reply.code(429).send({ error: 'Too many requests' });
  }
});
```

## preHandler Hook

Most common hook, execute after validation, before handler:

```typescript
// Authorization check
app.addHook('preHandler', async (request, reply) => {
  const { userId } = request.params as { userId: string };

  if (request.user.id !== userId && !request.user.isAdmin) {
    reply.code(403).send({ error: 'Forbidden' });
  }
});

// Load related data
app.addHook('preHandler', async (request, reply) => {
  if (request.params?.projectId) {
    request.project = await db.projects.findById(request.params.projectId);
    if (!request.project) {
      reply.code(404).send({ error: 'Project not found' });
    }
  }
});

// Transaction wrapper
app.addHook('preHandler', async (request) => {
  request.transaction = await db.beginTransaction();
});

app.addHook('onResponse', async (request) => {
  if (request.transaction) {
    await request.transaction.commit();
  }
});

app.addHook('onError', async (request, reply, error) => {
  if (request.transaction) {
    await request.transaction.rollback();
  }
});
```

## preSerialization Hook

Modify payload before serialization:

```typescript
app.addHook('preSerialization', async (request, reply, payload) => {
  // Add metadata to all responses
  if (payload && typeof payload === 'object') {
    return {
      ...payload,
      _meta: {
        requestId: request.id,
        timestamp: new Date().toISOString(),
      },
    };
  }
  return payload;
});

// Remove sensitive fields
app.addHook('preSerialization', async (request, reply, payload) => {
  if (payload?.user?.password) {
    const { password, ...user } = payload.user;
    return { ...payload, user };
  }
  return payload;
});
```

## onSend Hook

Modify response after serialization:

```typescript
app.addHook('onSend', async (request, reply, payload) => {
  // Add response headers
  reply.header('X-Response-Time', Date.now() - request.startTime);

  // Compress response
  if (payload && payload.length > 1024) {
    const compressed = await gzip(payload);
    reply.header('Content-Encoding', 'gzip');
    return compressed;
  }

  return payload;
});

// Transform JSON string response
app.addHook('onSend', async (request, reply, payload) => {
  if (reply.getHeader('content-type')?.includes('application/json')) {
    // payload is already a string at this point
    return payload;
  }
  return payload;
});
```

## onResponse Hook

Execute after response is sent. Cannot modify response:

```typescript
app.addHook('onResponse', async (request, reply) => {
  // Log response time
  const responseTime = Date.now() - request.startTime;
  request.log.info({
    method: request.method,
    url: request.url,
    statusCode: reply.statusCode,
    responseTime,
  }, 'Request completed');

  // Track metrics
  metrics.histogram('http_request_duration', responseTime, {
    method: request.method,
    route: request.routeOptions.url,
    status: reply.statusCode,
  });
});
```

## onError Hook

Execute when an error is thrown:

```typescript
app.addHook('onError', async (request, reply, error) => {
  // Log error details
  request.log.error({
    err: error,
    url: request.url,
    method: request.method,
    body: request.body,
  }, 'Request error');

  // Track error metrics
  metrics.increment('http_errors', {
    error: error.code || 'UNKNOWN',
    route: request.routeOptions.url,
  });

  // Cleanup resources
  if (request.tempFile) {
    await fs.unlink(request.tempFile).catch(() => {});
  }
});
```

## onTimeout Hook

Execute when request times out:

```typescript
const app = Fastify({
  connectionTimeout: 30000, // 30 seconds
});

app.addHook('onTimeout', async (request, reply) => {
  request.log.warn({
    url: request.url,
    method: request.method,
  }, 'Request timeout');

  // Cleanup
  if (request.abortController) {
    request.abortController.abort();
  }
});
```

## onRequestAbort Hook

Execute when client closes connection:

```typescript
app.addHook('onRequestAbort', async (request) => {
  request.log.info('Client aborted request');

  // Cancel ongoing operations
  if (request.abortController) {
    request.abortController.abort();
  }

  // Cleanup uploaded files
  if (request.uploadedFiles) {
    for (const file of request.uploadedFiles) {
      await fs.unlink(file.path).catch(() => {});
    }
  }
});
```

## Application Lifecycle Hooks

Hooks that run at application startup/shutdown:

```typescript
// After all plugins are loaded
app.addHook('onReady', async function () {
  this.log.info('Server is ready');

  // Initialize connections
  await this.db.connect();
  await this.redis.connect();

  // Warm caches
  await this.cache.warmup();
});

// When server is closing
app.addHook('onClose', async function () {
  this.log.info('Server is closing');

  // Cleanup connections
  await this.db.close();
  await this.redis.disconnect();
});

// After routes are registered
app.addHook('onRoute', (routeOptions) => {
  console.log(`Route registered: ${routeOptions.method} ${routeOptions.url}`);

  // Track all routes
  routes.push({
    method: routeOptions.method,
    url: routeOptions.url,
    schema: routeOptions.schema,
  });
});

// After plugin is registered
app.addHook('onRegister', (instance, options) => {
  console.log(`Plugin registered with prefix: ${options.prefix}`);
});
```

## Scoped Hooks

Hooks are scoped to their encapsulation context:

```typescript
app.addHook('onRequest', async (request) => {
  // Runs for ALL routes
  request.log.info('Global hook');
});

app.register(async function adminRoutes(fastify) {
  // Only runs for routes in this plugin
  fastify.addHook('onRequest', async (request, reply) => {
    if (!request.user?.isAdmin) {
      reply.code(403).send({ error: 'Admin only' });
    }
  });

  fastify.get('/admin/users', async () => {
    return { users: [] };
  });
}, { prefix: '/admin' });
```

## Hook Execution Order

Multiple hooks of the same type execute in registration order:

```typescript
app.addHook('onRequest', async () => {
  console.log('First');
});

app.addHook('onRequest', async () => {
  console.log('Second');
});

app.addHook('onRequest', async () => {
  console.log('Third');
});

// Output: First, Second, Third
```

## Stopping Hook Execution

Return early from hooks to stop processing:

```typescript
app.addHook('preHandler', async (request, reply) => {
  if (!request.user) {
    // Send response and return to stop further processing
    reply.code(401).send({ error: 'Unauthorized' });
    return;
  }
  // Continue to next hook and handler
});
```

## Route-Level Hooks

Add hooks to specific routes:

```typescript
const adminOnlyHook = async (request, reply) => {
  if (!request.user?.isAdmin) {
    reply.code(403).send({ error: 'Forbidden' });
  }
};

app.get('/admin/settings', {
  preHandler: [adminOnlyHook],
  handler: async (request) => {
    return { settings: {} };
  },
});

// Multiple hooks
app.post('/orders', {
  preValidation: [validateApiKey],
  preHandler: [loadUser, checkQuota, logOrder],
  handler: createOrderHandler,
});
```

## Async Hook Patterns

Always use async/await in hooks:

```typescript
// GOOD - async hook
app.addHook('preHandler', async (request, reply) => {
  const user = await loadUser(request.headers.authorization);
  request.user = user;
});

// AVOID - callback style (deprecated)
app.addHook('preHandler', (request, reply, done) => {
  loadUser(request.headers.authorization)
    .then((user) => {
      request.user = user;
      done();
    })
    .catch(done);
});
```
rules/http-proxy.md
---
name: http-proxy
description: HTTP proxying and reply.from() in Fastify
metadata:
  tags: proxy, gateway, reverse-proxy, microservices
---

# HTTP Proxy and Reply.from()

## Contents

- [@fastify/http-proxy](#fastifyhttp-proxy)
- [@fastify/reply-from](#fastifyreply-from)
- [API Gateway Pattern](#api-gateway-pattern)
- [Request Body Handling](#request-body-handling)
- [Error Handling](#error-handling)
- [WebSocket Proxying](#websocket-proxying)
- [Timeout Configuration](#timeout-configuration)
- [Caching Proxied Responses](#caching-proxied-responses)

## @fastify/http-proxy

Use `@fastify/http-proxy` for simple reverse proxy scenarios:

```typescript
import Fastify from 'fastify';
import httpProxy from '@fastify/http-proxy';

const app = Fastify({ logger: true });

// Proxy all requests to /api/* to another service
app.register(httpProxy, {
  upstream: 'http://backend-service:3001',
  prefix: '/api',
  rewritePrefix: '/v1',
  http2: false,
});

// With authentication
app.register(httpProxy, {
  upstream: 'http://internal-api:3002',
  prefix: '/internal',
  preHandler: async (request, reply) => {
    // Verify authentication before proxying
    if (!request.headers.authorization) {
      reply.code(401).send({ error: 'Unauthorized' });
    }
  },
});

await app.listen({ port: 3000 });
```

## @fastify/reply-from

For more control over proxying, use `@fastify/reply-from` with `reply.from()`:

```typescript
import Fastify from 'fastify';
import replyFrom from '@fastify/reply-from';

const app = Fastify({ logger: true });

app.register(replyFrom, {
  base: 'http://backend-service:3001',
  http2: false,
});

// Proxy with request/response manipulation
app.get('/users/:id', async (request, reply) => {
  const { id } = request.params;

  return reply.from(`/api/users/${id}`, {
    // Modify request before forwarding
    rewriteRequestHeaders: (originalReq, headers) => ({
      ...headers,
      'x-request-id': request.id,
      'x-forwarded-for': request.ip,
    }),
    // Modify response before sending
    onResponse: (request, reply, res) => {
      reply.header('x-proxy', 'fastify');
      reply.send(res);
    },
  });
});

// Conditional routing
app.all('/api/*', async (request, reply) => {
  const upstream = selectUpstream(request);

  return reply.from(request.url, {
    base: upstream,
  });
});

function selectUpstream(request) {
  // Route to different backends based on request
  if (request.headers['x-beta']) {
    return 'http://beta-backend:3001';
  }
  return 'http://stable-backend:3001';
}
```

## API Gateway Pattern

Build an API gateway with multiple backends:

```typescript
import Fastify from 'fastify';
import replyFrom from '@fastify/reply-from';

const app = Fastify({ logger: true });

// Configure multiple upstreams
const services = {
  users: 'http://users-service:3001',
  orders: 'http://orders-service:3002',
  products: 'http://products-service:3003',
};

app.register(replyFrom);

// Route to user service
app.register(async function (fastify) {
  fastify.all('/*', async (request, reply) => {
    return reply.from(request.url.replace('/users', ''), {
      base: services.users,
    });
  });
}, { prefix: '/users' });

// Route to orders service
app.register(async function (fastify) {
  fastify.all('/*', async (request, reply) => {
    return reply.from(request.url.replace('/orders', ''), {
      base: services.orders,
    });
  });
}, { prefix: '/orders' });

// Route to products service
app.register(async function (fastify) {
  fastify.all('/*', async (request, reply) => {
    return reply.from(request.url.replace('/products', ''), {
      base: services.products,
    });
  });
}, { prefix: '/products' });
```

## Request Body Handling

Handle request bodies when proxying:

```typescript
app.post('/api/data', async (request, reply) => {
  return reply.from('/data', {
    body: request.body,
    contentType: request.headers['content-type'],
  });
});

// Stream large bodies
app.post('/upload', async (request, reply) => {
  return reply.from('/upload', {
    body: request.raw,
    contentType: request.headers['content-type'],
  });
});
```

## Error Handling

Handle upstream errors gracefully:

```typescript
app.register(replyFrom, {
  base: 'http://backend:3001',
  // Called when upstream returns an error
  onError: (reply, error) => {
    reply.log.error({ err: error }, 'Proxy error');
    reply.code(502).send({
      error: 'Bad Gateway',
      message: 'Upstream service unavailable',
    });
  },
});

// Custom error handling per route
app.get('/data', async (request, reply) => {
  try {
    return await reply.from('/data');
  } catch (error) {
    request.log.error({ err: error }, 'Failed to proxy request');
    return reply.code(503).send({
      error: 'Service Unavailable',
      retryAfter: 30,
    });
  }
});
```

## WebSocket Proxying

Proxy WebSocket connections:

```typescript
import Fastify from 'fastify';
import httpProxy from '@fastify/http-proxy';

const app = Fastify({ logger: true });

app.register(httpProxy, {
  upstream: 'http://ws-backend:3001',
  prefix: '/ws',
  websocket: true,
});
```

## Timeout Configuration

Configure proxy timeouts:

```typescript
app.register(replyFrom, {
  base: 'http://backend:3001',
  http: {
    requestOptions: {
      timeout: 30000, // 30 seconds
    },
  },
});
```

## Caching Proxied Responses

Add caching to proxied responses:

```typescript
import { createCache } from 'async-cache-dedupe';

const cache = createCache({
  ttl: 60,
  storage: { type: 'memory' },
});

cache.define('proxyGet', async (url: string) => {
  const response = await fetch(`http://backend:3001${url}`);
  return response.json();
});

app.get('/cached/*', async (request, reply) => {
  const data = await cache.proxyGet(request.url);
  return data;
});
```
rules/logging.md
---
name: logging
description: Logging with Pino in Fastify
metadata:
  tags: logging, pino, debugging, observability
---

# Logging with Pino

## Contents

- [Built-in Pino Integration](#built-in-pino-integration)
- [Log Levels](#log-levels)
- [Request-Scoped Logging](#request-scoped-logging)
- [Structured Logging](#structured-logging)
- [Logging Configuration by Environment](#logging-configuration-by-environment)
- [Custom Serializers](#custom-serializers)
- [Redacting Sensitive Data](#redacting-sensitive-data)
- [Child Loggers](#child-loggers)
- [Request Logging Configuration](#request-logging-configuration)
- [Logging Errors](#logging-errors)
- [Log Destinations](#log-destinations)
- [Log Rotation](#log-rotation)
- [Log Aggregation](#log-aggregation)
- [Request ID Tracking](#request-id-tracking)
- [Performance Considerations](#performance-considerations)

## Built-in Pino Integration

Fastify uses Pino for high-performance logging:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  logger: true, // Enable default logging
});

// Or with configuration
const app = Fastify({
  logger: {
    level: 'info',
    transport: {
      target: 'pino-pretty',
      options: {
        colorize: true,
      },
    },
  },
});
```

## Log Levels

Available log levels (in order of severity):

```typescript
app.log.trace('Detailed debugging');
app.log.debug('Debugging information');
app.log.info('General information');
app.log.warn('Warning messages');
app.log.error('Error messages');
app.log.fatal('Fatal errors');
```

## Request-Scoped Logging

Each request has its own logger with request context:

```typescript
app.get('/users/:id', async (request) => {
  // Logs include request ID automatically
  request.log.info('Fetching user');

  const user = await db.users.findById(request.params.id);

  if (!user) {
    request.log.warn({ userId: request.params.id }, 'User not found');
    return { error: 'Not found' };
  }

  request.log.info({ userId: user.id }, 'User fetched');
  return user;
});
```

## Structured Logging

Always use structured logging with objects:

```typescript
// GOOD - structured, searchable
request.log.info({
  action: 'user_created',
  userId: user.id,
  email: user.email,
}, 'User created successfully');

request.log.error({
  err: error,
  userId: request.params.id,
  operation: 'fetch_user',
}, 'Failed to fetch user');

// BAD - unstructured, hard to parse
request.log.info(`User ${user.id} created with email ${user.email}`);
request.log.error(`Failed to fetch user: ${error.message}`);
```

## Logging Configuration by Environment

```typescript
function getLoggerConfig() {
  if (process.env.NODE_ENV === 'production') {
    return {
      level: 'info',
      // JSON output for log aggregation
    };
  }

  if (process.env.NODE_ENV === 'test') {
    return false; // Disable logging in tests
  }

  // Development
  return {
    level: 'debug',
    transport: {
      target: 'pino-pretty',
      options: {
        colorize: true,
        translateTime: 'HH:MM:ss Z',
        ignore: 'pid,hostname',
      },
    },
  };
}

const app = Fastify({
  logger: getLoggerConfig(),
});
```

## Custom Serializers

Customize how objects are serialized:

```typescript
const app = Fastify({
  logger: {
    level: 'info',
    serializers: {
      // Customize request serialization
      req: (request) => ({
        method: request.method,
        url: request.url,
        headers: {
          host: request.headers.host,
          'user-agent': request.headers['user-agent'],
        },
        remoteAddress: request.ip,
      }),

      // Customize response serialization
      res: (response) => ({
        statusCode: response.statusCode,
      }),

      // Custom serializer for users
      user: (user) => ({
        id: user.id,
        email: user.email,
        // Exclude sensitive fields
      }),
    },
  },
});

// Use custom serializer
request.log.info({ user: request.user }, 'User action');
```

## Redacting Sensitive Data

Prevent logging sensitive information:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  logger: {
    level: 'info',
    redact: {
      paths: [
        'req.headers.authorization',
        'req.headers.cookie',
        'body.password',
        'body.creditCard',
        '*.password',
        '*.secret',
        '*.token',
      ],
      censor: '[REDACTED]',
    },
  },
});
```

## Child Loggers

Create child loggers with additional context:

```typescript
app.addHook('onRequest', async (request) => {
  // Add user context to all logs for this request
  if (request.user) {
    request.log = request.log.child({
      userId: request.user.id,
      userRole: request.user.role,
    });
  }
});

// Service-level child logger
const userService = {
  log: app.log.child({ service: 'UserService' }),

  async create(data) {
    this.log.info({ email: data.email }, 'Creating user');
    // ...
  },
};
```

## Request Logging Configuration

Customize automatic request logging:

```typescript
const app = Fastify({
  logger: true,
  disableRequestLogging: true, // Disable default request/response logs
});

// Custom request logging
app.addHook('onRequest', async (request) => {
  request.log.info({
    method: request.method,
    url: request.url,
    query: request.query,
  }, 'Request received');
});

app.addHook('onResponse', async (request, reply) => {
  request.log.info({
    statusCode: reply.statusCode,
    responseTime: reply.elapsedTime,
  }, 'Request completed');
});
```

## Logging Errors

Properly log errors with stack traces:

```typescript
app.setErrorHandler((error, request, reply) => {
  // Log error with full details
  request.log.error({
    err: error, // Pino serializes error objects properly
    url: request.url,
    method: request.method,
    body: request.body,
    query: request.query,
  }, 'Request error');

  reply.code(error.statusCode || 500).send({
    error: error.message,
  });
});

// In handlers
app.get('/data', async (request) => {
  try {
    return await fetchData();
  } catch (error) {
    request.log.error({ err: error }, 'Failed to fetch data');
    throw error;
  }
});
```

## Log Destinations

Configure where logs are sent:

```typescript
import { createWriteStream } from 'node:fs';

// File output
const app = Fastify({
  logger: {
    level: 'info',
    stream: createWriteStream('./app.log'),
  },
});

// Multiple destinations with pino.multistream
import pino from 'pino';

const streams = [
  { stream: process.stdout },
  { stream: createWriteStream('./app.log') },
  { level: 'error', stream: createWriteStream('./error.log') },
];

const app = Fastify({
  logger: pino({ level: 'info' }, pino.multistream(streams)),
});
```

## Log Rotation

Use pino-roll for log rotation:

```bash
node app.js | pino-roll --frequency daily --extension .log
```

Or configure programmatically:

```typescript
import { createStream } from 'rotating-file-stream';

const stream = createStream('app.log', {
  size: '10M',     // Rotate every 10MB
  interval: '1d',  // Rotate daily
  compress: 'gzip',
  path: './logs',
});

const app = Fastify({
  logger: {
    level: 'info',
    stream,
  },
});
```

## Log Aggregation

Format logs for aggregation services:

```typescript
// For ELK Stack, Datadog, etc. - use default JSON format
const app = Fastify({
  logger: {
    level: 'info',
    // Default JSON output works with most log aggregators
  },
});

// Add service metadata
const app = Fastify({
  logger: {
    level: 'info',
    base: {
      service: 'user-api',
      version: process.env.APP_VERSION,
      environment: process.env.NODE_ENV,
    },
  },
});
```

## Request ID Tracking

Use request IDs for distributed tracing:

```typescript
const app = Fastify({
  logger: true,
  requestIdHeader: 'x-request-id', // Use incoming header
  genReqId: (request) => {
    // Generate ID if not provided
    return request.headers['x-request-id'] || crypto.randomUUID();
  },
});

// Forward request ID to downstream services
app.addHook('onRequest', async (request) => {
  request.requestId = request.id;
});

// Include in outgoing requests
const response = await fetch('http://other-service/api', {
  headers: {
    'x-request-id': request.id,
  },
});
```

## Performance Considerations

Pino is fast, but consider:

```typescript
// Avoid string concatenation in log calls
// BAD
request.log.info('User ' + user.id + ' did ' + action);

// GOOD
request.log.info({ userId: user.id, action }, 'User action');

// Use appropriate log levels
// Don't log at info level in hot paths
if (app.log.isLevelEnabled('debug')) {
  request.log.debug({ details: expensiveToCompute() }, 'Debug info');
}
```
rules/performance.md
---
name: performance
description: Performance optimization for Fastify applications
metadata:
  tags: performance, optimization, speed, benchmarking
---

# Performance Optimization

## Contents

- [Fastify is Fast by Default](#fastify-is-fast-by-default)
- [Use @fastify/under-pressure for Load Shedding](#use-fastifyunder-pressure-for-load-shedding)
- [Always Define Response Schemas](#always-define-response-schemas)
- [Avoid Dynamic Schema Compilation](#avoid-dynamic-schema-compilation)
- [Use Logger Wisely](#use-logger-wisely)
- [Connection Pooling](#connection-pooling)
- [Avoid Blocking the Event Loop](#avoid-blocking-the-event-loop)
- [Stream Large Responses](#stream-large-responses)
- [Caching Strategies](#caching-strategies)
- [Request Coalescing with async-cache-dedupe](#request-coalescing-with-async-cache-dedupe)
- [Payload Limits](#payload-limits)
- [Compression](#compression)
- [Connection Timeouts](#connection-timeouts)
- [Disable Unnecessary Features](#disable-unnecessary-features)
- [Benchmarking](#benchmarking)
- [Profiling](#profiling)
- [Memory Management](#memory-management)

## Fastify is Fast by Default

Fastify is designed for performance. Key optimizations are built-in:

- Fast JSON serialization with `fast-json-stringify`
- Efficient routing with `find-my-way`
- Schema-based validation with `ajv` (compiled validators)
- Low overhead request/response handling

## Use @fastify/under-pressure for Load Shedding

Protect your application from overload with `@fastify/under-pressure`:

```typescript
import underPressure from '@fastify/under-pressure';

app.register(underPressure, {
  maxEventLoopDelay: 1000,        // Max event loop delay in ms
  maxHeapUsedBytes: 1000000000,   // Max heap used (~1GB)
  maxRssBytes: 1500000000,        // Max RSS (~1.5GB)
  maxEventLoopUtilization: 0.98, // Max event loop utilization
  pressureHandler: (request, reply, type, value) => {
    reply.code(503).send({
      error: 'Service Unavailable',
      message: `Server under pressure: ${type}`,
    });
  },
});

// Health check that respects pressure
app.get('/health', async (request, reply) => {
  return { status: 'ok' };
});
```

## Always Define Response Schemas

Response schemas enable fast-json-stringify, which is significantly faster than JSON.stringify:

```typescript
// FAST - uses fast-json-stringify
app.get('/users', {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: { type: 'string' },
            name: { type: 'string' },
            email: { type: 'string' },
          },
        },
      },
    },
  },
}, async () => {
  return db.users.findAll();
});

// SLOW - uses JSON.stringify
app.get('/users-slow', async () => {
  return db.users.findAll();
});
```

## Avoid Dynamic Schema Compilation

Add schemas at startup, not at request time:

```typescript
// GOOD - schemas compiled at startup
app.addSchema({ $id: 'user', ... });

app.get('/users', {
  schema: { response: { 200: { $ref: 'user#' } } },
}, handler);

// BAD - schema compiled per request
app.get('/users', async (request, reply) => {
  const schema = getSchemaForUser(request.user);
  // This is slow!
});
```

## Use Logger Wisely

Pino is fast, but excessive logging has overhead:

```typescript
import Fastify from 'fastify';

// Set log level via environment variable
const app = Fastify({
  logger: {
    level: process.env.LOG_LEVEL || 'info',
  },
});

// Avoid logging large objects
app.get('/data', async (request) => {
  // BAD - logs entire payload
  request.log.info({ data: largeObject }, 'Processing');

  // GOOD - log only what's needed
  request.log.info({ id: largeObject.id }, 'Processing');

  return largeObject;
});
```

## Connection Pooling

Use connection pools for databases:

```typescript
import postgres from 'postgres';

// Create pool at startup
const sql = postgres(process.env.DATABASE_URL, {
  max: 20, // Maximum pool size
  idle_timeout: 20,
  connect_timeout: 10,
});

app.decorate('db', sql);

// Connections are reused
app.get('/users', async () => {
  return app.db`SELECT * FROM users LIMIT 100`;
});
```

## Avoid Blocking the Event Loop

Use `piscina` for CPU-intensive operations. It provides a robust worker thread pool:

```typescript
import Piscina from 'piscina';
import { join } from 'node:path';

const piscina = new Piscina({
  filename: join(import.meta.dirname, 'workers', 'compute.js'),
});

app.post('/compute', async (request) => {
  const result = await piscina.run(request.body);
  return result;
});
```

```typescript
// workers/compute.js
export default function compute(data) {
  // CPU-intensive work here
  return processedResult;
}
```

## Stream Large Responses

Stream large payloads instead of buffering:

```typescript
import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

// GOOD - stream file
app.get('/large-file', async (request, reply) => {
  const stream = createReadStream('./large-file.json');
  reply.type('application/json');
  return reply.send(stream);
});

// BAD - load entire file into memory
app.get('/large-file-bad', async () => {
  const content = await fs.readFile('./large-file.json', 'utf-8');
  return JSON.parse(content);
});

// Stream database results
app.get('/export', async (request, reply) => {
  reply.type('application/json');

  const cursor = db.users.findCursor();
  reply.raw.write('[');

  let first = true;
  for await (const user of cursor) {
    if (!first) reply.raw.write(',');
    reply.raw.write(JSON.stringify(user));
    first = false;
  }

  reply.raw.write(']');
  reply.raw.end();
});
```

## Caching Strategies

Implement caching for expensive operations:

```typescript
import { LRUCache } from 'lru-cache';

const cache = new LRUCache<string, unknown>({
  max: 1000,
  ttl: 60000, // 1 minute
});

app.get('/expensive/:id', async (request) => {
  const { id } = request.params;
  const cacheKey = `expensive:${id}`;

  const cached = cache.get(cacheKey);
  if (cached) {
    return cached;
  }

  const result = await expensiveOperation(id);
  cache.set(cacheKey, result);

  return result;
});

// Cache control headers
app.get('/static-data', async (request, reply) => {
  reply.header('Cache-Control', 'public, max-age=3600');
  return { data: 'static' };
});
```

## Request Coalescing with async-cache-dedupe

Use `async-cache-dedupe` for deduplicating concurrent identical requests and caching:

```typescript
import { createCache } from 'async-cache-dedupe';

const cache = createCache({
  ttl: 60, // seconds
  stale: 5, // serve stale while revalidating
  storage: { type: 'memory' },
});

cache.define('fetchData', async (id: string) => {
  return db.findById(id);
});

app.get('/data/:id', async (request) => {
  const { id } = request.params;
  // Automatically deduplicates concurrent requests for the same id
  // and caches the result
  return cache.fetchData(id);
});
```

For distributed caching, use Redis storage:

```typescript
import { createCache } from 'async-cache-dedupe';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

const cache = createCache({
  ttl: 60,
  storage: { type: 'redis', options: { client: redis } },
});
```

## Payload Limits

Set appropriate payload limits:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  bodyLimit: 1048576, // 1MB default
});

// Per-route limit for file uploads
app.post('/upload', {
  bodyLimit: 10485760, // 10MB for this route
}, uploadHandler);
```

## Compression

Use compression for responses:

```typescript
import fastifyCompress from '@fastify/compress';

app.register(fastifyCompress, {
  global: true,
  threshold: 1024, // Only compress responses > 1KB
  encodings: ['gzip', 'deflate'],
});

// Disable for specific route
app.get('/already-compressed', {
  compress: false,
}, handler);
```

## Connection Timeouts

Configure appropriate timeouts:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  connectionTimeout: 30000, // 30 seconds
  keepAliveTimeout: 5000,   // 5 seconds
});

// Per-route timeout
app.get('/long-operation', {
  config: {
    timeout: 60000, // 60 seconds
  },
}, async (request) => {
  return longOperation();
});
```

## Disable Unnecessary Features

Disable features you don't need:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  disableRequestLogging: true, // If you don't need request logs
  trustProxy: false,           // If not behind proxy
  caseSensitive: true,         // Enable for slight performance gain
  ignoreDuplicateSlashes: false,
});
```

## Benchmarking

Use autocannon for load testing:

```bash
# Install
npm install -g autocannon

# Basic benchmark
autocannon http://localhost:3000/api/users

# With options
autocannon -c 100 -d 30 -p 10 http://localhost:3000/api/users
# -c: connections
# -d: duration in seconds
# -p: pipelining factor
```

```typescript
// Programmatic benchmarking
import autocannon from 'autocannon';

const result = await autocannon({
  url: 'http://localhost:3000/api/users',
  connections: 100,
  duration: 30,
  pipelining: 10,
});

console.log(autocannon.printResult(result));
```

## Profiling

Use `@platformatic/flame` for flame graph profiling:

```bash
npx @platformatic/flame app.js
```

This generates an interactive flame graph to identify performance bottlenecks.

## Memory Management

Monitor and optimize memory usage:

```typescript
// Add health endpoint with memory info
app.get('/health', async () => {
  const memory = process.memoryUsage();
  return {
    status: 'ok',
    memory: {
      heapUsed: Math.round(memory.heapUsed / 1024 / 1024) + 'MB',
      heapTotal: Math.round(memory.heapTotal / 1024 / 1024) + 'MB',
      rss: Math.round(memory.rss / 1024 / 1024) + 'MB',
    },
  };
});

// Avoid memory leaks in closures
app.addHook('onRequest', async (request) => {
  // BAD - holding reference to large object
  const largeData = await loadLargeData();
  request.getData = () => largeData;

  // GOOD - load on demand
  request.getData = () => loadLargeData();
});
```
rules/plugins.md
---
name: plugins
description: Plugin development and encapsulation in Fastify
metadata:
  tags: plugins, encapsulation, modules, architecture
---

# Plugin Development and Encapsulation

## Contents

- [Understanding Encapsulation](#understanding-encapsulation)
- [Breaking Encapsulation with fastify-plugin](#breaking-encapsulation-with-fastify-plugin)
- [Plugin Registration Order](#plugin-registration-order)
- [Plugin Options](#plugin-options)
- [Plugin Factory Pattern](#plugin-factory-pattern)
- [Plugin Dependencies](#plugin-dependencies)
- [Scoped Plugins for Route Groups](#scoped-plugins-for-route-groups)
- [Prefix Routes with Register](#prefix-routes-with-register)
- [Plugin Metadata](#plugin-metadata)
- [Autoload Plugins](#autoload-plugins)
- [Testing Plugins in Isolation](#testing-plugins-in-isolation)

## Understanding Encapsulation

Fastify's plugin system provides automatic encapsulation. Each plugin creates its own context, isolating decorators, hooks, and plugins registered within it:

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

## Breaking Encapsulation with fastify-plugin

Use `fastify-plugin` when you need to share decorators, hooks, or plugins with the parent context:

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

## Plugin Registration Order

Plugins are registered in order, but loading is asynchronous. Use `after()` for sequential dependencies:

```typescript
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();
```

## Plugin Options

Always validate and document plugin options:

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

## Plugin Factory Pattern

Create configurable plugins using factory functions:

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

## Plugin Dependencies

Declare dependencies to ensure proper load order:

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

## Scoped Plugins for Route Groups

Use encapsulation to scope plugins to specific routes:

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

## Prefix Routes with Register

Use the `prefix` option to namespace routes:

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

## Plugin Metadata

Add metadata for documentation and tooling:

```typescript
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: [],
  },
});
```

## Autoload Plugins

Use `@fastify/autoload` for automatic plugin loading:

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

## Testing Plugins in Isolation

Test plugins independently:

```typescript
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'));
  });
});
```
rules/routes.md
---
name: routes
description: Route organization and handlers in Fastify
metadata:
  tags: routes, handlers, http, rest, api
---

# Route Organization and Handlers

## Contents

- [Basic Route Definition](#basic-route-definition)
- [Route Parameters](#route-parameters)
- [Query String Parameters](#query-string-parameters)
- [Request Body](#request-body)
- [Headers](#headers)
- [Reply Methods](#reply-methods)
- [Route Organization by Feature](#route-organization-by-feature)
- [Route Constraints](#route-constraints)
- [Route Prefixing](#route-prefixing)
- [Multiple Methods](#multiple-methods)
- [404 Handler](#404-handler)
- [Method Not Allowed](#method-not-allowed)
- [Route-Level Configuration](#route-level-configuration)
- [Async Route Registration](#async-route-registration)
- [Auto-loading Routes with @fastify/autoload](#auto-loading-routes-with-fastifyautoload)

## Basic Route Definition

Define routes with the shorthand methods or the full route method:

```typescript
import Fastify from 'fastify';

const app = Fastify();

// Shorthand methods
app.get('/users', async (request, reply) => {
  return { users: [] };
});

app.post('/users', async (request, reply) => {
  return { created: true };
});

// Full route method with all options
app.route({
  method: 'GET',
  url: '/users/:id',
  schema: {
    params: {
      type: 'object',
      properties: {
        id: { type: 'string' },
      },
      required: ['id'],
    },
  },
  handler: async (request, reply) => {
    return { id: request.params.id };
  },
});
```

## Route Parameters

Access URL parameters through `request.params`:

```typescript
// Single parameter
app.get('/users/:id', async (request) => {
  const { id } = request.params as { id: string };
  return { userId: id };
});

// Multiple parameters
app.get('/users/:userId/posts/:postId', async (request) => {
  const { userId, postId } = request.params as { userId: string; postId: string };
  return { userId, postId };
});

// Wildcard parameter (captures everything after)
app.get('/files/*', async (request) => {
  const path = (request.params as { '*': string })['*'];
  return { filePath: path };
});

// Regex parameters (Fastify uses find-my-way)
app.get('/orders/:id(\\d+)', async (request) => {
  // Only matches numeric IDs
  const { id } = request.params as { id: string };
  return { orderId: parseInt(id, 10) };
});
```

## Query String Parameters

Access query parameters through `request.query`:

```typescript
app.get('/search', {
  schema: {
    querystring: {
      type: 'object',
      properties: {
        q: { type: 'string' },
        page: { type: 'integer', default: 1 },
        limit: { type: 'integer', default: 10, maximum: 100 },
      },
      required: ['q'],
    },
  },
  handler: async (request) => {
    const { q, page, limit } = request.query as {
      q: string;
      page: number;
      limit: number;
    };
    return { query: q, page, limit };
  },
});
```

## Request Body

Access the request body through `request.body`:

```typescript
app.post('/users', {
  schema: {
    body: {
      type: 'object',
      properties: {
        name: { type: 'string', minLength: 1 },
        email: { type: 'string', format: 'email' },
        age: { type: 'integer', minimum: 0 },
      },
      required: ['name', 'email'],
    },
  },
  handler: async (request, reply) => {
    const user = request.body as { name: string; email: string; age?: number };
    // Create user...
    reply.code(201);
    return { user };
  },
});
```

## Headers

Access request headers through `request.headers`:

```typescript
app.get('/protected', {
  schema: {
    headers: {
      type: 'object',
      properties: {
        authorization: { type: 'string' },
      },
      required: ['authorization'],
    },
  },
  handler: async (request) => {
    const token = request.headers.authorization;
    return { authenticated: true };
  },
});
```

## Reply Methods

Use reply methods to control the response:

```typescript
app.get('/examples', async (request, reply) => {
  // Set status code
  reply.code(201);

  // Set headers
  reply.header('X-Custom-Header', 'value');
  reply.headers({ 'X-Another': 'value', 'X-Third': 'value' });

  // Set content type
  reply.type('application/json');

  // Redirect
  // reply.redirect('/other-url');
  // reply.redirect(301, '/permanent-redirect');

  // Return response (automatic serialization)
  return { status: 'ok' };
});

// Explicit send (useful in non-async handlers)
app.get('/explicit', (request, reply) => {
  reply.send({ status: 'ok' });
});

// Stream response
app.get('/stream', async (request, reply) => {
  const stream = fs.createReadStream('./large-file.txt');
  reply.type('text/plain');
  return reply.send(stream);
});
```

## Route Organization by Feature

Organize routes by feature/domain in separate files:

```
src/
  routes/
    users/
      index.ts       # Route definitions
      handlers.ts    # Handler functions
      schemas.ts     # JSON schemas
    posts/
      index.ts
      handlers.ts
      schemas.ts
```

```typescript
// routes/users/schemas.ts
export const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'string', format: 'uuid' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
  },
};

export const createUserSchema = {
  body: {
    type: 'object',
    properties: {
      name: { type: 'string', minLength: 1 },
      email: { type: 'string', format: 'email' },
    },
    required: ['name', 'email'],
  },
  response: {
    201: userSchema,
  },
};

// routes/users/handlers.ts
import type { FastifyRequest, FastifyReply } from 'fastify';

export async function createUser(
  request: FastifyRequest<{ Body: { name: string; email: string } }>,
  reply: FastifyReply,
) {
  const { name, email } = request.body;
  const user = await request.server.db.users.create({ name, email });
  reply.code(201);
  return user;
}

export async function getUsers(request: FastifyRequest) {
  return request.server.db.users.findAll();
}

// routes/users/index.ts
import type { FastifyInstance } from 'fastify';
import { createUser, getUsers } from './handlers.js';
import { createUserSchema } from './schemas.js';

export default async function userRoutes(fastify: FastifyInstance) {
  fastify.get('/', getUsers);
  fastify.post('/', { schema: createUserSchema }, createUser);
}
```

## Route Constraints

Add constraints to routes for versioning or host-based routing:

```typescript
// Version constraint
app.get('/users', {
  constraints: { version: '1.0.0' },
  handler: async () => ({ version: '1.0.0', users: [] }),
});

app.get('/users', {
  constraints: { version: '2.0.0' },
  handler: async () => ({ version: '2.0.0', data: { users: [] } }),
});

// Client sends: Accept-Version: 1.0.0

// Host constraint
app.get('/', {
  constraints: { host: 'api.example.com' },
  handler: async () => ({ api: true }),
});

app.get('/', {
  constraints: { host: 'www.example.com' },
  handler: async () => ({ web: true }),
});
```

## Route Prefixing

Use prefixes to namespace routes:

```typescript
// Using register
app.register(async function (fastify) {
  fastify.get('/list', async () => ({ users: [] }));
  fastify.get('/:id', async (request) => ({ id: request.params.id }));
}, { prefix: '/users' });

// Results in:
// GET /users/list
// GET /users/:id
```

## Multiple Methods

Handle multiple HTTP methods with one handler:

```typescript
app.route({
  method: ['GET', 'HEAD'],
  url: '/resource',
  handler: async (request) => {
    return { data: 'resource' };
  },
});
```

## 404 Handler

Customize the not found handler:

```typescript
app.setNotFoundHandler({
  preValidation: async (request, reply) => {
    // Optional pre-validation hook
  },
  preHandler: async (request, reply) => {
    // Optional pre-handler hook
  },
}, async (request, reply) => {
  reply.code(404);
  return {
    error: 'Not Found',
    message: `Route ${request.method} ${request.url} not found`,
    statusCode: 404,
  };
});
```

## Method Not Allowed

Handle method not allowed responses:

```typescript
// Fastify doesn't have built-in 405 handling
// Implement with a custom not found handler that checks allowed methods
app.setNotFoundHandler(async (request, reply) => {
  // Check if the URL exists with a different method
  const route = app.hasRoute({
    url: request.url,
    method: 'GET', // Check other methods
  });

  if (route) {
    reply.code(405);
    return { error: 'Method Not Allowed' };
  }

  reply.code(404);
  return { error: 'Not Found' };
});
```

## Route-Level Configuration

Apply configuration to specific routes:

```typescript
app.get('/slow-operation', {
  config: {
    rateLimit: { max: 10, timeWindow: '1 minute' },
  },
  handler: async (request) => {
    return { result: await slowOperation() };
  },
});

// Access config in hooks
app.addHook('onRequest', async (request, reply) => {
  const config = request.routeOptions.config;
  if (config.rateLimit) {
    // Apply rate limiting
  }
});
```

## Async Route Registration

Register routes from async sources:

```typescript
app.register(async function (fastify) {
  const routeConfigs = await loadRoutesFromDatabase();

  for (const config of routeConfigs) {
    fastify.route({
      method: config.method,
      url: config.path,
      handler: createDynamicHandler(config),
    });
  }
});
```

## Auto-loading Routes with @fastify/autoload

Use `@fastify/autoload` to automatically load routes from a directory structure:

```typescript
import Fastify from 'fastify';
import autoload from '@fastify/autoload';
import { join } from 'node:path';

const app = Fastify({ logger: true });

// Auto-load plugins
app.register(autoload, {
  dir: join(import.meta.dirname, 'plugins'),
  options: { prefix: '' },
});

// Auto-load routes
app.register(autoload, {
  dir: join(import.meta.dirname, 'routes'),
  options: { prefix: '/api' },
});

await app.listen({ port: 3000 });
```

Directory structure:

```
src/
  plugins/
    database.ts     # Loaded automatically
    auth.ts         # Loaded automatically
  routes/
    users/
      index.ts      # GET/POST /api/users
      _id/
        index.ts    # GET/PUT/DELETE /api/users/:id
    posts/
      index.ts      # GET/POST /api/posts
```

Route file example:

```typescript
// routes/users/index.ts
import type { FastifyPluginAsync } from 'fastify';

const users: FastifyPluginAsync = async (fastify) => {
  fastify.get('/', async () => {
    return fastify.repositories.users.findAll();
  });

  fastify.post('/', async (request) => {
    return fastify.repositories.users.create(request.body);
  });
};

export default users;
```
rules/schemas.md
---
name: schemas
description: JSON Schema validation in Fastify with TypeBox
metadata:
  tags: validation, json-schema, schemas, ajv, typebox
---

# JSON Schema Validation

## Contents

- [Use TypeBox for Type-Safe Schemas](#use-typebox-for-type-safe-schemas)
- [TypeBox Common Patterns](#typebox-common-patterns)
- [Register TypeBox Schemas Globally](#register-typebox-schemas-globally)
- [Plain JSON Schema (Alternative)](#plain-json-schema-alternative)
- [Request Validation Parts](#request-validation-parts)
- [Shared Schemas with $id](#shared-schemas-with-id)
- [Array Schemas](#array-schemas)
- [Custom Formats](#custom-formats)
- [Custom Keywords](#custom-keywords)
- [Coercion](#coercion)
- [Validation Error Handling](#validation-error-handling)
- [Schema Compiler Options](#schema-compiler-options)
- [Nullable Fields](#nullable-fields)
- [Conditional Validation](#conditional-validation)
- [Schema Organization](#schema-organization)
- [OpenAPI/Swagger Integration](#openapiswagger-integration)
- [Performance Considerations](#performance-considerations)

## Use TypeBox for Type-Safe Schemas

**Prefer TypeBox for defining schemas.** It provides TypeScript types automatically and compiles to JSON Schema:

```typescript
import Fastify from 'fastify';
import { Type, type Static } from '@sinclair/typebox';

const app = Fastify();

// Define schema with TypeBox - get TypeScript types for free
const CreateUserBody = Type.Object({
  name: Type.String({ minLength: 1, maxLength: 100 }),
  email: Type.String({ format: 'email' }),
  age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
});

const UserResponse = Type.Object({
  id: Type.String({ format: 'uuid' }),
  name: Type.String(),
  email: Type.String(),
  createdAt: Type.String({ format: 'date-time' }),
});

// TypeScript types are derived automatically
type CreateUserBodyType = Static<typeof CreateUserBody>;
type UserResponseType = Static<typeof UserResponse>;

app.post<{
  Body: CreateUserBodyType;
  Reply: UserResponseType;
}>('/users', {
  schema: {
    body: CreateUserBody,
    response: {
      201: UserResponse,
    },
  },
}, async (request, reply) => {
  // request.body is fully typed as CreateUserBodyType
  const user = await createUser(request.body);
  reply.code(201);
  return user;
});
```

## TypeBox Common Patterns

```typescript
import { Type, type Static } from '@sinclair/typebox';

// Enums
const Status = Type.Union([
  Type.Literal('active'),
  Type.Literal('inactive'),
  Type.Literal('pending'),
]);

// Arrays
const Tags = Type.Array(Type.String(), { minItems: 1, maxItems: 10 });

// Nested objects
const Address = Type.Object({
  street: Type.String(),
  city: Type.String(),
  country: Type.String(),
  zip: Type.Optional(Type.String()),
});

// References (reusable schemas)
const User = Type.Object({
  id: Type.String({ format: 'uuid' }),
  name: Type.String(),
  address: Address,
  tags: Tags,
  status: Status,
});

// Nullable
const NullableString = Type.Union([Type.String(), Type.Null()]);

// Record/Map
const Metadata = Type.Record(Type.String(), Type.Unknown());
```

## Register TypeBox Schemas Globally

```typescript
import { Type, type Static } from '@sinclair/typebox';

// Define shared schemas
const ErrorResponse = Type.Object({
  error: Type.String(),
  message: Type.String(),
  statusCode: Type.Integer(),
});

const PaginationQuery = Type.Object({
  page: Type.Integer({ minimum: 1, default: 1 }),
  limit: Type.Integer({ minimum: 1, maximum: 100, default: 20 }),
});

// Register globally
app.addSchema(Type.Object({ $id: 'ErrorResponse', ...ErrorResponse }));
app.addSchema(Type.Object({ $id: 'PaginationQuery', ...PaginationQuery }));

// Reference in routes
app.get('/items', {
  schema: {
    querystring: { $ref: 'PaginationQuery#' },
    response: {
      400: { $ref: 'ErrorResponse#' },
    },
  },
}, handler);
```

## Plain JSON Schema (Alternative)

You can also use plain JSON Schema directly:

```typescript
import Fastify from 'fastify';

const app = Fastify();

const createUserSchema = {
  body: {
    type: 'object',
    properties: {
      name: { type: 'string', minLength: 1, maxLength: 100 },
      email: { type: 'string', format: 'email' },
      age: { type: 'integer', minimum: 0, maximum: 150 },
    },
    required: ['name', 'email'],
    additionalProperties: false,
  },
  response: {
    201: {
      type: 'object',
      properties: {
        id: { type: 'string', format: 'uuid' },
        name: { type: 'string' },
        email: { type: 'string' },
        createdAt: { type: 'string', format: 'date-time' },
      },
    },
  },
};

app.post('/users', { schema: createUserSchema }, async (request, reply) => {
  const user = await createUser(request.body);
  reply.code(201);
  return user;
});
```

## Request Validation Parts

Validate different parts of the request:

```typescript
const fullRequestSchema = {
  // URL parameters
  params: {
    type: 'object',
    properties: {
      id: { type: 'string', format: 'uuid' },
    },
    required: ['id'],
  },

  // Query string
  querystring: {
    type: 'object',
    properties: {
      include: { type: 'string', enum: ['posts', 'comments', 'all'] },
      limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
    },
  },

  // Request headers
  headers: {
    type: 'object',
    properties: {
      'x-api-key': { type: 'string', minLength: 32 },
    },
    required: ['x-api-key'],
  },

  // Request body
  body: {
    type: 'object',
    properties: {
      data: { type: 'object' },
    },
    required: ['data'],
  },
};

app.put('/resources/:id', { schema: fullRequestSchema }, handler);
```

## Shared Schemas with $id

Define reusable schemas with `$id` and reference them with `$ref`:

```typescript
// Add shared schemas to Fastify
app.addSchema({
  $id: 'user',
  type: 'object',
  properties: {
    id: { type: 'string', format: 'uuid' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
    createdAt: { type: 'string', format: 'date-time' },
  },
  required: ['id', 'name', 'email'],
});

app.addSchema({
  $id: 'userCreate',
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1 },
    email: { type: 'string', format: 'email' },
  },
  required: ['name', 'email'],
  additionalProperties: false,
});

app.addSchema({
  $id: 'error',
  type: 'object',
  properties: {
    statusCode: { type: 'integer' },
    error: { type: 'string' },
    message: { type: 'string' },
  },
});

// Reference shared schemas
app.post('/users', {
  schema: {
    body: { $ref: 'userCreate#' },
    response: {
      201: { $ref: 'user#' },
      400: { $ref: 'error#' },
    },
  },
}, handler);

app.get('/users/:id', {
  schema: {
    params: {
      type: 'object',
      properties: { id: { type: 'string', format: 'uuid' } },
      required: ['id'],
    },
    response: {
      200: { $ref: 'user#' },
      404: { $ref: 'error#' },
    },
  },
}, handler);
```

## Array Schemas

Define schemas for array responses:

```typescript
app.addSchema({
  $id: 'userList',
  type: 'object',
  properties: {
    users: {
      type: 'array',
      items: { $ref: 'user#' },
    },
    total: { type: 'integer' },
    page: { type: 'integer' },
    pageSize: { type: 'integer' },
  },
});

app.get('/users', {
  schema: {
    querystring: {
      type: 'object',
      properties: {
        page: { type: 'integer', minimum: 1, default: 1 },
        pageSize: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
      },
    },
    response: {
      200: { $ref: 'userList#' },
    },
  },
}, handler);
```

## Custom Formats

Add custom validation formats:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  ajv: {
    customOptions: {
      formats: {
        'iso-country': /^[A-Z]{2}$/,
        'phone': /^\+?[1-9]\d{1,14}$/,
      },
    },
  },
});

// Or add formats dynamically
app.addSchema({
  $id: 'address',
  type: 'object',
  properties: {
    street: { type: 'string' },
    country: { type: 'string', format: 'iso-country' },
    phone: { type: 'string', format: 'phone' },
  },
});
```

## Custom Keywords

Add custom validation keywords:

```typescript
import Fastify from 'fastify';
import Ajv from 'ajv';

const app = Fastify({
  ajv: {
    customOptions: {
      keywords: [
        {
          keyword: 'isEven',
          type: 'number',
          validate: (schema: boolean, data: number) => {
            if (schema) {
              return data % 2 === 0;
            }
            return true;
          },
          errors: false,
        },
      ],
    },
  },
});

// Use custom keyword
app.post('/numbers', {
  schema: {
    body: {
      type: 'object',
      properties: {
        value: { type: 'integer', isEven: true },
      },
    },
  },
}, handler);
```

## Coercion

Fastify coerces types by default for query strings and params:

```typescript
// Query string "?page=5&active=true" becomes:
// { page: 5, active: true } (number and boolean, not strings)

app.get('/items', {
  schema: {
    querystring: {
      type: 'object',
      properties: {
        page: { type: 'integer' },      // "5" -> 5
        active: { type: 'boolean' },    // "true" -> true
        tags: {
          type: 'array',
          items: { type: 'string' },    // "a,b,c" -> ["a", "b", "c"]
        },
      },
    },
  },
}, handler);
```

## Validation Error Handling

Customize validation error responses:

```typescript
app.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    reply.code(400).send({
      error: 'Validation Error',
      message: 'Request validation failed',
      details: error.validation.map((err) => ({
        field: err.instancePath || err.params?.missingProperty,
        message: err.message,
        keyword: err.keyword,
      })),
    });
    return;
  }

  // Handle other errors
  reply.code(error.statusCode || 500).send({
    error: error.name,
    message: error.message,
  });
});
```

## Schema Compiler Options

Configure the Ajv schema compiler:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  ajv: {
    customOptions: {
      removeAdditional: 'all',   // Remove extra properties
      useDefaults: true,         // Apply default values
      coerceTypes: true,         // Coerce types
      allErrors: true,           // Report all errors, not just first
    },
    plugins: [
      require('ajv-formats'),    // Add format validators
    ],
  },
});
```

## Nullable Fields

Handle nullable fields properly:

```typescript
app.addSchema({
  $id: 'profile',
  type: 'object',
  properties: {
    name: { type: 'string' },
    bio: { type: ['string', 'null'] },  // Can be string or null
    avatar: {
      oneOf: [
        { type: 'string', format: 'uri' },
        { type: 'null' },
      ],
    },
  },
});
```

## Conditional Validation

Use if/then/else for conditional validation:

```typescript
app.addSchema({
  $id: 'payment',
  type: 'object',
  properties: {
    method: { type: 'string', enum: ['card', 'bank'] },
    cardNumber: { type: 'string' },
    bankAccount: { type: 'string' },
  },
  required: ['method'],
  if: {
    properties: { method: { const: 'card' } },
  },
  then: {
    required: ['cardNumber'],
  },
  else: {
    required: ['bankAccount'],
  },
});
```

## Schema Organization

Organize schemas in a dedicated file:

```typescript
// schemas/index.ts
export const schemas = [
  {
    $id: 'user',
    type: 'object',
    properties: {
      id: { type: 'string', format: 'uuid' },
      name: { type: 'string' },
      email: { type: 'string', format: 'email' },
    },
  },
  {
    $id: 'error',
    type: 'object',
    properties: {
      statusCode: { type: 'integer' },
      error: { type: 'string' },
      message: { type: 'string' },
    },
  },
];

// app.ts
import { schemas } from './schemas/index.js';

for (const schema of schemas) {
  app.addSchema(schema);
}
```

## OpenAPI/Swagger Integration

Schemas work directly with @fastify/swagger:

```typescript
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUi from '@fastify/swagger-ui';

app.register(fastifySwagger, {
  openapi: {
    info: {
      title: 'My API',
      version: '1.0.0',
    },
  },
});

app.register(fastifySwaggerUi, {
  routePrefix: '/docs',
});

// Schemas are automatically converted to OpenAPI definitions
```

## Performance Considerations

Response schemas enable fast-json-stringify for serialization:

```typescript
// With response schema - uses fast-json-stringify (faster)
app.get('/users', {
  schema: {
    response: {
      200: {
        type: 'array',
        items: { $ref: 'user#' },
      },
    },
  },
}, handler);

// Without response schema - uses JSON.stringify (slower)
app.get('/users-slow', handler);
```

Always define response schemas for production APIs to benefit from optimized serialization.
rules/serialization.md
---
name: serialization
description: Response serialization in Fastify with TypeBox
metadata:
  tags: serialization, response, json, fast-json-stringify, typebox
---

# Response Serialization

## Contents

- [Use TypeBox for Type-Safe Response Schemas](#use-typebox-for-type-safe-response-schemas)
- [Fast JSON Stringify](#fast-json-stringify)
- [Response Schema Benefits](#response-schema-benefits)
- [Multiple Status Code Schemas](#multiple-status-code-schemas)
- [Default Response Schema](#default-response-schema)
- [Custom Serializers](#custom-serializers)
- [Shared Serializers](#shared-serializers)
- [Serialization with Type Coercion](#serialization-with-type-coercion)
- [Nullable Fields](#nullable-fields)
- [Additional Properties](#additional-properties)
- [Nested Objects](#nested-objects)
- [Date Serialization](#date-serialization)
- [BigInt Serialization](#bigint-serialization)
- [Stream Responses](#stream-responses)
- [Pre-Serialization Hook](#pre-serialization-hook)
- [Disable Serialization](#disable-serialization)

## Use TypeBox for Type-Safe Response Schemas

Define response schemas with TypeBox for automatic TypeScript types and fast serialization:

```typescript
import Fastify from 'fastify';
import { Type, type Static } from '@sinclair/typebox';

const app = Fastify();

// Define response schema with TypeBox
const UserResponse = Type.Object({
  id: Type.String(),
  name: Type.String(),
  email: Type.String(),
});

const UsersResponse = Type.Array(UserResponse);

type UserResponseType = Static<typeof UserResponse>;

// With TypeBox schema - uses fast-json-stringify (faster) + TypeScript types
app.get<{ Reply: Static<typeof UsersResponse> }>('/users', {
  schema: {
    response: {
      200: UsersResponse,
    },
  },
}, async () => {
  return db.users.findAll();
});

// Without schema - uses JSON.stringify (slower), no type safety
app.get('/users-slow', async () => {
  return db.users.findAll();
});
```

## Fast JSON Stringify

Fastify uses `fast-json-stringify` when response schemas are defined. This provides:

1. **Performance**: 2-3x faster serialization than JSON.stringify
2. **Security**: Only defined properties are serialized (strips sensitive data)
3. **Type coercion**: Ensures output matches the schema
4. **TypeScript**: Full type inference with TypeBox

## Response Schema Benefits

1. **Performance**: 2-3x faster serialization
2. **Security**: Only defined properties are included
3. **Documentation**: OpenAPI/Swagger integration
4. **Type coercion**: Ensures correct output types

```typescript
app.get('/user/:id', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          name: { type: 'string' },
          // password is NOT in schema, so it's stripped
        },
      },
    },
  },
}, async (request) => {
  const user = await db.users.findById(request.params.id);
  // Even if user has password field, it won't be serialized
  return user;
});
```

## Multiple Status Code Schemas

Define schemas for different response codes:

```typescript
app.get('/users/:id', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          name: { type: 'string' },
          email: { type: 'string' },
        },
      },
      404: {
        type: 'object',
        properties: {
          statusCode: { type: 'integer' },
          error: { type: 'string' },
          message: { type: 'string' },
        },
      },
    },
  },
}, async (request, reply) => {
  const user = await db.users.findById(request.params.id);

  if (!user) {
    reply.code(404);
    return { statusCode: 404, error: 'Not Found', message: 'User not found' };
  }

  return user;
});
```

## Default Response Schema

Use 'default' for common error responses:

```typescript
app.get('/resource', {
  schema: {
    response: {
      200: { $ref: 'resource#' },
      '4xx': {
        type: 'object',
        properties: {
          statusCode: { type: 'integer' },
          error: { type: 'string' },
          message: { type: 'string' },
        },
      },
      '5xx': {
        type: 'object',
        properties: {
          statusCode: { type: 'integer' },
          error: { type: 'string' },
        },
      },
    },
  },
}, handler);
```

## Custom Serializers

Create custom serialization functions:

```typescript
// Per-route serializer
app.get('/custom', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          value: { type: 'string' },
        },
      },
    },
  },
  serializerCompiler: ({ schema }) => {
    return (data) => {
      // Custom serialization logic
      return JSON.stringify({
        value: String(data.value).toUpperCase(),
        serializedAt: new Date().toISOString(),
      });
    };
  },
}, async () => {
  return { value: 'hello' };
});
```

## Shared Serializers

Use the global serializer compiler:

```typescript
import Fastify from 'fastify';

const app = Fastify({
  serializerCompiler: ({ schema, method, url, httpStatus }) => {
    // Custom compilation logic
    const stringify = fastJson(schema);
    return (data) => stringify(data);
  },
});
```

## Serialization with Type Coercion

fast-json-stringify coerces types:

```typescript
app.get('/data', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          count: { type: 'integer' },    // '5' -> 5
          active: { type: 'boolean' },   // 'true' -> true
          tags: {
            type: 'array',
            items: { type: 'string' },   // [1, 2] -> ['1', '2']
          },
        },
      },
    },
  },
}, async () => {
  return {
    count: '5',      // Coerced to integer
    active: 'true',  // Coerced to boolean
    tags: [1, 2, 3], // Coerced to strings
  };
});
```

## Nullable Fields

Handle nullable fields properly:

```typescript
app.get('/profile', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          bio: { type: ['string', 'null'] },
          avatar: {
            oneOf: [
              { type: 'string', format: 'uri' },
              { type: 'null' },
            ],
          },
        },
      },
    },
  },
}, async () => {
  return {
    name: 'John',
    bio: null,
    avatar: null,
  };
});
```

## Additional Properties

Control extra properties in response:

```typescript
// Strip additional properties (default)
app.get('/strict', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          name: { type: 'string' },
        },
        additionalProperties: false,
      },
    },
  },
}, async () => {
  return { id: '1', name: 'John', secret: 'hidden' };
  // Output: { "id": "1", "name": "John" }
});

// Allow additional properties
app.get('/flexible', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' },
        },
        additionalProperties: true,
      },
    },
  },
}, async () => {
  return { id: '1', extra: 'included' };
  // Output: { "id": "1", "extra": "included" }
});
```

## Nested Objects

Serialize nested structures:

```typescript
app.addSchema({
  $id: 'address',
  type: 'object',
  properties: {
    street: { type: 'string' },
    city: { type: 'string' },
    country: { type: 'string' },
  },
});

app.get('/user', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          address: { $ref: 'address#' },
          contacts: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                type: { type: 'string' },
                value: { type: 'string' },
              },
            },
          },
        },
      },
    },
  },
}, async () => {
  return {
    name: 'John',
    address: { street: '123 Main', city: 'Boston', country: 'USA' },
    contacts: [
      { type: 'email', value: '[email protected]' },
      { type: 'phone', value: '+1234567890' },
    ],
  };
});
```

## Date Serialization

Handle dates consistently:

```typescript
app.get('/events', {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            name: { type: 'string' },
            date: { type: 'string', format: 'date-time' },
          },
        },
      },
    },
  },
}, async () => {
  const events = await db.events.findAll();

  // Convert Date objects to ISO strings
  return events.map((e) => ({
    ...e,
    date: e.date.toISOString(),
  }));
});
```

## BigInt Serialization

Handle BigInt values:

```typescript
// BigInt is not JSON serializable by default
app.get('/large-number', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' }, // Serialize as string
          count: { type: 'integer' },
        },
      },
    },
  },
}, async () => {
  const bigValue = 9007199254740993n;

  return {
    id: bigValue.toString(), // Convert to string
    count: Number(bigValue), // Or number if safe
  };
});
```

## Stream Responses

Stream responses bypass serialization:

```typescript
import { createReadStream } from 'node:fs';

app.get('/file', async (request, reply) => {
  const stream = createReadStream('./data.json');
  reply.type('application/json');
  return reply.send(stream);
});

// Streaming JSON array
app.get('/stream', async (request, reply) => {
  reply.type('application/json');

  const cursor = db.users.findCursor();

  reply.raw.write('[');
  let first = true;

  for await (const user of cursor) {
    if (!first) reply.raw.write(',');
    reply.raw.write(JSON.stringify(user));
    first = false;
  }

  reply.raw.write(']');
  reply.raw.end();
});
```

## Pre-Serialization Hook

Modify data before serialization:

```typescript
app.addHook('preSerialization', async (request, reply, payload) => {
  // Add metadata to responses
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
    return {
      ...payload,
      _links: {
        self: request.url,
      },
    };
  }
  return payload;
});
```

## Disable Serialization

Skip serialization for specific routes:

```typescript
app.get('/raw', async (request, reply) => {
  const data = JSON.stringify({ raw: true });
  reply.type('application/json');
  reply.serializer((payload) => payload); // Pass through
  return data;
});
```
rules/testing.md
---
name: testing
description: Testing Fastify applications with inject()
metadata:
  tags: testing, inject, node-test, integration, unit
---

# Testing Fastify Applications

## Contents

- [Using inject() for Request Testing](#using-inject-for-request-testing)
- [Testing with Headers and Authentication](#testing-with-headers-and-authentication)
- [Testing Query Parameters](#testing-query-parameters)
- [Testing URL Parameters](#testing-url-parameters)
- [Testing Validation Errors](#testing-validation-errors)
- [Testing File Uploads](#testing-file-uploads)
- [Testing Streams](#testing-streams)
- [Mocking Dependencies](#mocking-dependencies)
- [Testing Plugins in Isolation](#testing-plugins-in-isolation)
- [Testing Hooks](#testing-hooks)
- [Test Factory Pattern](#test-factory-pattern)
- [Database Testing with Transactions](#database-testing-with-transactions)
- [Parallel Test Execution](#parallel-test-execution)
- [Running Tests](#running-tests)

## Using inject() for Request Testing

Fastify's `inject()` method simulates HTTP requests without network overhead:

```typescript
import { describe, it, before, after } from 'node:test';
import Fastify from 'fastify';
import { buildApp } from './app.js';

describe('User API', () => {
  let app;

  before(async () => {
    app = await buildApp();
    await app.ready();
  });

  after(async () => {
    await app.close();
  });

  it('should return users list', async (t) => {
    const response = await app.inject({
      method: 'GET',
      url: '/users',
    });

    t.assert.equal(response.statusCode, 200);
    t.assert.equal(response.headers['content-type'], 'application/json; charset=utf-8');

    const body = response.json();
    t.assert.ok(Array.isArray(body.users));
  });

  it('should create a user', async (t) => {
    const response = await app.inject({
      method: 'POST',
      url: '/users',
      payload: {
        name: 'John Doe',
        email: '[email protected]',
      },
    });

    t.assert.equal(response.statusCode, 201);

    const body = response.json();
    t.assert.equal(body.name, 'John Doe');
    t.assert.ok(body.id);
  });
});
```

## Testing with Headers and Authentication

Test authenticated endpoints:

```typescript
describe('Protected Routes', () => {
  let app;
  let authToken;

  before(async () => {
    app = await buildApp();
    await app.ready();

    // Get auth token
    const loginResponse = await app.inject({
      method: 'POST',
      url: '/auth/login',
      payload: {
        email: '[email protected]',
        password: 'password123',
      },
    });

    authToken = loginResponse.json().token;
  });

  after(async () => {
    await app.close();
  });

  it('should reject unauthenticated requests', async (t) => {
    const response = await app.inject({
      method: 'GET',
      url: '/profile',
    });

    t.assert.equal(response.statusCode, 401);
  });

  it('should return profile for authenticated user', async (t) => {
    const response = await app.inject({
      method: 'GET',
      url: '/profile',
      headers: {
        authorization: `Bearer ${authToken}`,
      },
    });

    t.assert.equal(response.statusCode, 200);
    t.assert.equal(response.json().email, '[email protected]');
  });
});
```

## Testing Query Parameters

Test routes with query strings:

```typescript
it('should filter users by status', async (t) => {
  const response = await app.inject({
    method: 'GET',
    url: '/users',
    query: {
      status: 'active',
      page: '1',
      limit: '10',
    },
  });

  t.assert.equal(response.statusCode, 200);
  const body = response.json();
  t.assert.ok(body.users.every((u) => u.status === 'active'));
});

// Or use URL with query string
it('should search users', async (t) => {
  const response = await app.inject({
    method: 'GET',
    url: '/users?q=john&sort=name',
  });

  t.assert.equal(response.statusCode, 200);
});
```

## Testing URL Parameters

Test routes with path parameters:

```typescript
it('should return user by id', async (t) => {
  const userId = 'user-123';

  const response = await app.inject({
    method: 'GET',
    url: `/users/${userId}`,
  });

  t.assert.equal(response.statusCode, 200);
  t.assert.equal(response.json().id, userId);
});

it('should return 404 for non-existent user', async (t) => {
  const response = await app.inject({
    method: 'GET',
    url: '/users/non-existent',
  });

  t.assert.equal(response.statusCode, 404);
});
```

## Testing Validation Errors

Test schema validation:

```typescript
describe('Validation', () => {
  it('should reject invalid email', async (t) => {
    const response = await app.inject({
      method: 'POST',
      url: '/users',
      payload: {
        name: 'John',
        email: 'not-an-email',
      },
    });

    t.assert.equal(response.statusCode, 400);
    const body = response.json();
    t.assert.ok(body.message.includes('email'));
  });

  it('should reject missing required fields', async (t) => {
    const response = await app.inject({
      method: 'POST',
      url: '/users',
      payload: {
        name: 'John',
        // missing email
      },
    });

    t.assert.equal(response.statusCode, 400);
  });

  it('should coerce query parameters', async (t) => {
    const response = await app.inject({
      method: 'GET',
      url: '/items?limit=10&active=true',
    });

    t.assert.equal(response.statusCode, 200);
    // limit is coerced to number, active to boolean
  });
});
```

## Testing File Uploads

Test multipart form data:

```typescript
import { createReadStream } from 'node:fs';
import FormData from 'form-data';

it('should upload file', async (t) => {
  const form = new FormData();
  form.append('file', createReadStream('./test/fixtures/test.pdf'));
  form.append('name', 'test-document');

  const response = await app.inject({
    method: 'POST',
    url: '/upload',
    payload: form,
    headers: form.getHeaders(),
  });

  t.assert.equal(response.statusCode, 200);
  t.assert.ok(response.json().fileId);
});
```

## Testing Streams

Test streaming responses:

```typescript
it('should stream large file', async (t) => {
  const response = await app.inject({
    method: 'GET',
    url: '/files/large-file',
  });

  t.assert.equal(response.statusCode, 200);
  t.assert.ok(response.rawPayload.length > 0);
});
```

## Mocking Dependencies

Mock external services and databases:

```typescript
import { describe, it, before, after, mock } from 'node:test';

describe('User Service', () => {
  let app;

  before(async () => {
    // Create app with mocked dependencies
    const mockDb = {
      users: {
        findAll: mock.fn(async () => [
          { id: '1', name: 'User 1' },
          { id: '2', name: 'User 2' },
        ]),
        findById: mock.fn(async (id) => {
          if (id === '1') return { id: '1', name: 'User 1' };
          return null;
        }),
        create: mock.fn(async (data) => ({ id: 'new-id', ...data })),
      },
    };

    app = Fastify();
    app.decorate('db', mockDb);
    app.register(import('./routes/users.js'));
    await app.ready();
  });

  after(async () => {
    await app.close();
  });

  it('should call findAll', async (t) => {
    const response = await app.inject({
      method: 'GET',
      url: '/users',
    });

    t.assert.equal(response.statusCode, 200);
    t.assert.equal(app.db.users.findAll.mock.calls.length, 1);
  });
});
```

## Testing Plugins in Isolation

Test plugins independently:

```typescript
import { describe, it, before, after } from 'node:test';
import Fastify from 'fastify';
import cachePlugin from './plugins/cache.js';

describe('Cache Plugin', () => {
  let app;

  before(async () => {
    app = Fastify();
    app.register(cachePlugin, { ttl: 1000 });
    await app.ready();
  });

  after(async () => {
    await app.close();
  });

  it('should decorate fastify with cache', (t) => {
    t.assert.ok(app.hasDecorator('cache'));
    t.assert.equal(typeof app.cache.get, 'function');
    t.assert.equal(typeof app.cache.set, 'function');
  });

  it('should cache and retrieve values', (t) => {
    app.cache.set('key', 'value');
    t.assert.equal(app.cache.get('key'), 'value');
  });
});
```

## Testing Hooks

Test hook behavior:

```typescript
describe('Hooks', () => {
  it('should add request id header', async (t) => {
    const response = await app.inject({
      method: 'GET',
      url: '/health',
    });

    t.assert.ok(response.headers['x-request-id']);
  });

  it('should log request timing', async (t) => {
    const logs = [];
    const app = Fastify({
      logger: {
        level: 'info',
        stream: {
          write: (msg) => logs.push(JSON.parse(msg)),
        },
      },
    });

    app.register(import('./app.js'));
    await app.ready();

    await app.inject({ method: 'GET', url: '/health' });

    const responseLog = logs.find((l) => l.msg?.includes('completed'));
    t.assert.ok(responseLog);
    t.assert.ok(responseLog.responseTime);

    await app.close();
  });
});
```

## Test Factory Pattern

Create a reusable test app builder:

```typescript
// test/helper.ts
import Fastify from 'fastify';
import type { FastifyInstance } from 'fastify';

interface TestContext {
  app: FastifyInstance;
  inject: FastifyInstance['inject'];
}

export async function buildTestApp(options = {}): Promise<TestContext> {
  const app = Fastify({
    logger: false, // Disable logging in tests
    ...options,
  });

  // Register plugins
  app.register(import('../src/plugins/database.js'), {
    connectionString: process.env.TEST_DATABASE_URL,
  });
  app.register(import('../src/routes/index.js'));

  await app.ready();

  return {
    app,
    inject: app.inject.bind(app),
  };
}

// Usage in tests
describe('API Tests', () => {
  let ctx: TestContext;

  before(async () => {
    ctx = await buildTestApp();
  });

  after(async () => {
    await ctx.app.close();
  });

  it('should work', async (t) => {
    const response = await ctx.inject({
      method: 'GET',
      url: '/health',
    });
    t.assert.equal(response.statusCode, 200);
  });
});
```

## Database Testing with Transactions

Use transactions for test isolation:

```typescript
describe('Database Integration', () => {
  let app;
  let transaction;

  before(async () => {
    app = await buildApp();
    await app.ready();
  });

  after(async () => {
    await app.close();
  });

  beforeEach(async () => {
    transaction = await app.db.beginTransaction();
    app.db.setTransaction(transaction);
  });

  afterEach(async () => {
    await transaction.rollback();
  });

  it('should create user', async (t) => {
    const response = await app.inject({
      method: 'POST',
      url: '/users',
      payload: { name: 'Test', email: '[email protected]' },
    });

    t.assert.equal(response.statusCode, 201);
    // Transaction is rolled back after test
  });
});
```

## Parallel Test Execution

Structure tests for parallel execution:

```typescript
// Tests run in parallel by default with node:test
// Use separate app instances or proper isolation

import { describe, it } from 'node:test';

describe('User API', async () => {
  // Each test suite gets its own app instance
  const app = await buildTestApp();

  it('test 1', async (t) => {
    // ...
  });

  it('test 2', async (t) => {
    // ...
  });

  // Cleanup after all tests in this suite
  after(() => app.close());
});

describe('Post API', async () => {
  const app = await buildTestApp();

  it('test 1', async (t) => {
    // ...
  });

  after(() => app.close());
});
```

## Running Tests

```bash
# Run all tests
node --test

# Run with TypeScript
node --test src/**/*.test.ts

# Run specific file
node --test src/routes/users.test.ts

# With coverage
node --test --experimental-test-coverage

# Watch mode
node --test --watch
```
rules/typescript.md
---
name: typescript
description: TypeScript integration with Fastify
metadata:
  tags: typescript, types, generics, type-safety
---

# TypeScript Integration

## Contents

- [Type Stripping with Node.js](#type-stripping-with-nodejs)
- [Basic Type Safety](#basic-type-safety)
- [Typing Route Handlers](#typing-route-handlers)
- [Type Providers](#type-providers)
- [Typing Decorators](#typing-decorators)
- [Typing Plugins](#typing-plugins)
- [Typing Hooks](#typing-hooks)
- [Typing Schema Objects](#typing-schema-objects)
- [Shared Types](#shared-types)
- [Type-Safe Route Registration](#type-safe-route-registration)
- [Avoiding Type Gymnastics](#avoiding-type-gymnastics)
- [Type Checking Without Compilation](#type-checking-without-compilation)

## Type Stripping with Node.js

Use Node.js built-in type stripping (Node.js 22.6+):

```bash
# Run TypeScript directly
node --experimental-strip-types app.ts

# In Node.js 23+
node app.ts
```

```json
// package.json
{
  "type": "module",
  "scripts": {
    "start": "node app.ts",
    "dev": "node --watch app.ts"
  }
}
```

```typescript
// tsconfig.json for type stripping
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "verbatimModuleSyntax": true,
    "erasableSyntaxOnly": true,
    "noEmit": true,
    "strict": true
  }
}
```

## Basic Type Safety

Type your Fastify application:

```typescript
import Fastify, { type FastifyInstance, type FastifyRequest, type FastifyReply } from 'fastify';

const app: FastifyInstance = Fastify({ logger: true });

app.get('/health', async (request: FastifyRequest, reply: FastifyReply) => {
  return { status: 'ok' };
});

await app.listen({ port: 3000 });
```

## Typing Route Handlers

Use generics to type request parts:

```typescript
import type { FastifyRequest, FastifyReply } from 'fastify';

interface CreateUserBody {
  name: string;
  email: string;
}

interface UserParams {
  id: string;
}

interface UserQuery {
  include?: string;
}

// Type the request with generics
app.post<{
  Body: CreateUserBody;
}>('/users', async (request, reply) => {
  const { name, email } = request.body; // Fully typed
  return { name, email };
});

app.get<{
  Params: UserParams;
  Querystring: UserQuery;
}>('/users/:id', async (request) => {
  const { id } = request.params;         // string
  const { include } = request.query;      // string | undefined
  return { id, include };
});

// Full route options typing
app.route<{
  Params: UserParams;
  Querystring: UserQuery;
  Body: CreateUserBody;
  Reply: { user: { id: string; name: string } };
}>({
  method: 'PUT',
  url: '/users/:id',
  handler: async (request, reply) => {
    return { user: { id: request.params.id, name: request.body.name } };
  },
});
```

## Type Providers

Use @fastify/type-provider-typebox for runtime + compile-time safety:

```typescript
import Fastify from 'fastify';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';

const app = Fastify().withTypeProvider<TypeBoxTypeProvider>();

const UserSchema = Type.Object({
  id: Type.String(),
  name: Type.String(),
  email: Type.String({ format: 'email' }),
});

const CreateUserSchema = Type.Object({
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: 'email' }),
});

app.post('/users', {
  schema: {
    body: CreateUserSchema,
    response: {
      201: UserSchema,
    },
  },
}, async (request, reply) => {
  // request.body is typed as { name: string; email: string }
  const { name, email } = request.body;

  reply.code(201);
  return { id: 'generated', name, email };
});
```

## Typing Decorators

Extend Fastify types with declaration merging:

```typescript
import Fastify from 'fastify';

// Declare types for decorators
declare module 'fastify' {
  interface FastifyInstance {
    config: {
      port: number;
      host: string;
    };
    db: Database;
  }

  interface FastifyRequest {
    user?: {
      id: string;
      email: string;
      role: string;
    };
    startTime: number;
  }

  interface FastifyReply {
    sendSuccess: (data: unknown) => void;
  }
}

const app = Fastify();

// Add decorators
app.decorate('config', { port: 3000, host: 'localhost' });
app.decorate('db', new Database());

app.decorateRequest('user', null);
app.decorateRequest('startTime', 0);

app.decorateReply('sendSuccess', function (data: unknown) {
  this.send({ success: true, data });
});

// Now fully typed
app.get('/profile', async (request, reply) => {
  const user = request.user; // { id: string; email: string; role: string } | undefined
  const config = app.config;  // { port: number; host: string }

  reply.sendSuccess({ user });
});
```

## Typing Plugins

Type plugin options and exports:

```typescript
import fp from 'fastify-plugin';
import type { FastifyPluginAsync } from 'fastify';

interface DatabasePluginOptions {
  connectionString: string;
  poolSize?: number;
}

declare module 'fastify' {
  interface FastifyInstance {
    db: {
      query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
      close: () => Promise<void>;
    };
  }
}

const databasePlugin: FastifyPluginAsync<DatabasePluginOptions> = async (
  fastify,
  options,
) => {
  const { connectionString, poolSize = 10 } = options;

  const db = await createConnection(connectionString, poolSize);

  fastify.decorate('db', {
    query: (sql: string, params?: unknown[]) => db.query(sql, params),
    close: () => db.end(),
  });

  fastify.addHook('onClose', async () => {
    await db.end();
  });
};

export default fp(databasePlugin, {
  name: 'database',
});
```

## Typing Hooks

Type hook functions:

```typescript
import type {
  FastifyRequest,
  FastifyReply,
  onRequestHookHandler,
  preHandlerHookHandler,
} from 'fastify';

const authHook: preHandlerHookHandler = async (
  request: FastifyRequest,
  reply: FastifyReply,
) => {
  const token = request.headers.authorization;
  if (!token) {
    reply.code(401).send({ error: 'Unauthorized' });
    return;
  }
  request.user = await verifyToken(token);
};

const timingHook: onRequestHookHandler = async (request) => {
  request.startTime = Date.now();
};

app.addHook('onRequest', timingHook);
app.addHook('preHandler', authHook);
```

## Typing Schema Objects

Create reusable typed schemas:

```typescript
import type { JSONSchema7 } from 'json-schema';

// Define schema with const assertion for type inference
const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
  },
  required: ['id', 'name', 'email'],
} as const satisfies JSONSchema7;

// Infer TypeScript type from schema
type User = {
  id: string;
  name: string;
  email: string;
};

app.get<{ Reply: User }>('/users/:id', {
  schema: {
    response: {
      200: userSchema,
    },
  },
}, async (request) => {
  return { id: '1', name: 'John', email: '[email protected]' };
});
```

## Shared Types

Organize types in dedicated files:

```typescript
// types/index.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

export interface CreateUserInput {
  name: string;
  email: string;
}

export interface PaginationQuery {
  page?: number;
  limit?: number;
  sort?: string;
}

// routes/users.ts
import type { FastifyInstance } from 'fastify';
import type { User, CreateUserInput, PaginationQuery } from '../types/index.js';

export default async function userRoutes(fastify: FastifyInstance) {
  fastify.get<{
    Querystring: PaginationQuery;
    Reply: { users: User[]; total: number };
  }>('/', async (request) => {
    const { page = 1, limit = 10 } = request.query;
    // ...
  });

  fastify.post<{
    Body: CreateUserInput;
    Reply: User;
  }>('/', async (request, reply) => {
    reply.code(201);
    // ...
  });
}
```

## Type-Safe Route Registration

Create typed route factories:

```typescript
import type { FastifyInstance, RouteOptions } from 'fastify';

function createCrudRoutes<T extends { id: string }>(
  fastify: FastifyInstance,
  options: {
    prefix: string;
    schema: {
      item: object;
      create: object;
      update: object;
    };
    handlers: {
      list: () => Promise<T[]>;
      get: (id: string) => Promise<T | null>;
      create: (data: unknown) => Promise<T>;
      update: (id: string, data: unknown) => Promise<T>;
      delete: (id: string) => Promise<void>;
    };
  },
) {
  const { prefix, schema, handlers } = options;

  fastify.get(`${prefix}`, {
    schema: { response: { 200: { type: 'array', items: schema.item } } },
  }, async () => handlers.list());

  fastify.get(`${prefix}/:id`, {
    schema: { response: { 200: schema.item } },
  }, async (request) => {
    const item = await handlers.get((request.params as { id: string }).id);
    if (!item) throw { statusCode: 404, message: 'Not found' };
    return item;
  });

  // ... more routes
}
```

## Avoiding Type Gymnastics

Keep types simple and practical:

```typescript
// GOOD - simple, readable types
interface UserRequest {
  Params: { id: string };
  Body: { name: string };
}

app.put<UserRequest>('/users/:id', handler);

// AVOID - overly complex generic types
type DeepPartial<T> = T extends object ? {
  [P in keyof T]?: DeepPartial<T[P]>;
} : T;

// AVOID - excessive type inference
type InferSchemaType<T> = T extends { properties: infer P }
  ? { [K in keyof P]: InferPropertyType<P[K]> }
  : never;
```

## Type Checking Without Compilation

Use TypeScript for type checking only:

```bash
# Type check without emitting
npx tsc --noEmit

# Watch mode
npx tsc --noEmit --watch

# In CI
npm run typecheck
```

```json
// package.json
{
  "scripts": {
    "start": "node app.ts",
    "typecheck": "tsc --noEmit",
    "test": "npm run typecheck && node --test"
  }
}
```
rules/websockets.md
---
name: websockets
description: WebSocket support in Fastify
metadata:
  tags: websockets, realtime, ws, socket
---

# WebSocket Support

## Contents

- [Using @fastify/websocket](#using-fastifywebsocket)
- [WebSocket with Hooks](#websocket-with-hooks)
- [Connection Options](#connection-options)
- [Broadcast to All Clients](#broadcast-to-all-clients)
- [Rooms/Channels Pattern](#roomschannels-pattern)
- [Structured Message Protocol](#structured-message-protocol)
- [Heartbeat/Ping-Pong](#heartbeatping-pong)
- [Authentication](#authentication)
- [Error Handling](#error-handling)
- [Rate Limiting WebSocket Messages](#rate-limiting-websocket-messages)
- [Graceful Shutdown](#graceful-shutdown)
- [Full-Duplex Stream Pattern](#full-duplex-stream-pattern)

## Using @fastify/websocket

Add WebSocket support to Fastify:

```typescript
import Fastify from 'fastify';
import websocket from '@fastify/websocket';

const app = Fastify();

app.register(websocket);

app.get('/ws', { websocket: true }, (socket, request) => {
  socket.on('message', (message) => {
    const data = message.toString();
    console.log('Received:', data);

    // Echo back
    socket.send(`Echo: ${data}`);
  });

  socket.on('close', () => {
    console.log('Client disconnected');
  });

  socket.on('error', (error) => {
    console.error('WebSocket error:', error);
  });
});

await app.listen({ port: 3000 });
```

## WebSocket with Hooks

Use Fastify hooks with WebSocket routes:

```typescript
app.register(async function wsRoutes(fastify) {
  // This hook runs before WebSocket upgrade
  fastify.addHook('preValidation', async (request, reply) => {
    const token = request.headers.authorization;
    if (!token) {
      reply.code(401).send({ error: 'Unauthorized' });
      return;
    }
    request.user = await verifyToken(token);
  });

  fastify.get('/ws', { websocket: true }, (socket, request) => {
    console.log('Connected user:', request.user.id);

    socket.on('message', (message) => {
      // Handle authenticated messages
    });
  });
});
```

## Connection Options

Configure WebSocket server options:

```typescript
app.register(websocket, {
  options: {
    maxPayload: 1048576, // 1MB max message size
    clientTracking: true,
    perMessageDeflate: {
      zlibDeflateOptions: {
        chunkSize: 1024,
        memLevel: 7,
        level: 3,
      },
      zlibInflateOptions: {
        chunkSize: 10 * 1024,
      },
    },
  },
});
```

## Broadcast to All Clients

Broadcast messages to connected clients:

```typescript
const clients = new Set<WebSocket>();

app.get('/ws', { websocket: true }, (socket, request) => {
  clients.add(socket);

  socket.on('close', () => {
    clients.delete(socket);
  });

  socket.on('message', (message) => {
    // Broadcast to all other clients
    for (const client of clients) {
      if (client !== socket && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    }
  });
});

// Broadcast from HTTP route
app.post('/broadcast', async (request) => {
  const { message } = request.body;

  for (const client of clients) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({ type: 'broadcast', message }));
    }
  }

  return { sent: clients.size };
});
```

## Rooms/Channels Pattern

Organize connections into rooms:

```typescript
const rooms = new Map<string, Set<WebSocket>>();

function joinRoom(socket: WebSocket, roomId: string) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId)!.add(socket);
}

function leaveRoom(socket: WebSocket, roomId: string) {
  rooms.get(roomId)?.delete(socket);
  if (rooms.get(roomId)?.size === 0) {
    rooms.delete(roomId);
  }
}

function broadcastToRoom(roomId: string, message: string, exclude?: WebSocket) {
  const room = rooms.get(roomId);
  if (!room) return;

  for (const client of room) {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  }
}

app.get('/ws/:roomId', { websocket: true }, (socket, request) => {
  const { roomId } = request.params as { roomId: string };

  joinRoom(socket, roomId);

  socket.on('message', (message) => {
    broadcastToRoom(roomId, message.toString(), socket);
  });

  socket.on('close', () => {
    leaveRoom(socket, roomId);
  });
});
```

## Structured Message Protocol

Use JSON for structured messages:

```typescript
interface WSMessage {
  type: string;
  payload?: unknown;
  id?: string;
}

app.get('/ws', { websocket: true }, (socket, request) => {
  function send(message: WSMessage) {
    socket.send(JSON.stringify(message));
  }

  socket.on('message', (raw) => {
    let message: WSMessage;

    try {
      message = JSON.parse(raw.toString());
    } catch {
      send({ type: 'error', payload: 'Invalid JSON' });
      return;
    }

    switch (message.type) {
      case 'ping':
        send({ type: 'pong', id: message.id });
        break;

      case 'subscribe':
        handleSubscribe(socket, message.payload);
        send({ type: 'subscribed', payload: message.payload, id: message.id });
        break;

      case 'message':
        handleMessage(socket, message.payload);
        break;

      default:
        send({ type: 'error', payload: 'Unknown message type' });
    }
  });
});
```

## Heartbeat/Ping-Pong

Keep connections alive:

```typescript
const HEARTBEAT_INTERVAL = 30000;
const clients = new Map<WebSocket, { isAlive: boolean }>();

app.get('/ws', { websocket: true }, (socket, request) => {
  clients.set(socket, { isAlive: true });

  socket.on('pong', () => {
    const client = clients.get(socket);
    if (client) client.isAlive = true;
  });

  socket.on('close', () => {
    clients.delete(socket);
  });
});

// Heartbeat interval
setInterval(() => {
  for (const [socket, state] of clients) {
    if (!state.isAlive) {
      socket.terminate();
      clients.delete(socket);
      continue;
    }

    state.isAlive = false;
    socket.ping();
  }
}, HEARTBEAT_INTERVAL);
```

## Authentication

Authenticate WebSocket connections:

```typescript
app.get('/ws', {
  websocket: true,
  preValidation: async (request, reply) => {
    // Authenticate via query parameter or header
    const token = request.query.token || request.headers.authorization?.replace('Bearer ', '');

    if (!token) {
      reply.code(401).send({ error: 'Token required' });
      return;
    }

    try {
      request.user = await verifyToken(token);
    } catch {
      reply.code(401).send({ error: 'Invalid token' });
    }
  },
}, (socket, request) => {
  console.log('Authenticated user:', request.user);

  socket.on('message', (message) => {
    // Handle authenticated messages
  });
});
```

## Error Handling

Handle WebSocket errors properly:

```typescript
app.get('/ws', { websocket: true }, (socket, request) => {
  socket.on('error', (error) => {
    request.log.error({ err: error }, 'WebSocket error');
  });

  socket.on('message', async (raw) => {
    try {
      const message = JSON.parse(raw.toString());
      const result = await processMessage(message);
      socket.send(JSON.stringify({ success: true, result }));
    } catch (error) {
      request.log.error({ err: error }, 'Message processing error');
      socket.send(JSON.stringify({
        success: false,
        error: error.message,
      }));
    }
  });
});
```

## Rate Limiting WebSocket Messages

Limit message frequency:

```typescript
const rateLimits = new Map<WebSocket, { count: number; resetAt: number }>();

function checkRateLimit(socket: WebSocket, limit: number, window: number): boolean {
  const now = Date.now();
  let state = rateLimits.get(socket);

  if (!state || now > state.resetAt) {
    state = { count: 0, resetAt: now + window };
    rateLimits.set(socket, state);
  }

  state.count++;

  if (state.count > limit) {
    return false;
  }

  return true;
}

app.get('/ws', { websocket: true }, (socket, request) => {
  socket.on('message', (message) => {
    if (!checkRateLimit(socket, 100, 60000)) {
      socket.send(JSON.stringify({ error: 'Rate limit exceeded' }));
      return;
    }

    // Process message
  });

  socket.on('close', () => {
    rateLimits.delete(socket);
  });
});
```

## Graceful Shutdown

Close WebSocket connections on shutdown:

```typescript
import closeWithGrace from 'close-with-grace';

const connections = new Set<WebSocket>();

app.get('/ws', { websocket: true }, (socket, request) => {
  connections.add(socket);

  socket.on('close', () => {
    connections.delete(socket);
  });
});

closeWithGrace({ delay: 5000 }, async ({ signal }) => {
  // Notify clients
  for (const socket of connections) {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'shutdown', message: 'Server is shutting down' }));
      socket.close(1001, 'Server shutdown');
    }
  }

  await app.close();
});
```

## Full-Duplex Stream Pattern

Use WebSocket for streaming data:

```typescript
app.get('/ws/stream', { websocket: true }, async (socket, request) => {
  const stream = createDataStream();

  stream.on('data', (data) => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'data', payload: data }));
    }
  });

  stream.on('end', () => {
    socket.send(JSON.stringify({ type: 'end' }));
    socket.close();
  });

  socket.on('message', (message) => {
    const { type, payload } = JSON.parse(message.toString());

    if (type === 'pause') {
      stream.pause();
    } else if (type === 'resume') {
      stream.resume();
    }
  });

  socket.on('close', () => {
    stream.destroy();
  });
});
```
SKILL.md
---
name: fastify-best-practices
description: "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."
metadata:
  tags: fastify, nodejs, typescript, backend, api, server, http
---

## When to use

Use this skill when you need to:
- Develop backend applications using Fastify
- Implement Fastify plugins and route handlers
- Get guidance on Fastify architecture and patterns
- Use TypeScript with Fastify (strip types)
- Implement testing with Fastify's inject method
- Configure validation, serialization, and error handling

## Quick Start

A minimal, runnable Fastify server to get started immediately:

```ts
import Fastify from 'fastify'

const app = Fastify({ logger: true })

app.get('/health', async (request, reply) => {
  return { status: 'ok' }
})

const start = async () => {
  await app.listen({ port: 3000, host: '0.0.0.0' })
}
start()
```

## Recommended Reading Order for Common Scenarios

- **New to Fastify?** Start with `plugins.md` → `routes.md` → `schemas.md`
- **Adding authentication:** `plugins.md` → `hooks.md` → `authentication.md`
- **Improving performance:** `schemas.md` → `serialization.md` → `performance.md`
- **Setting up testing:** `routes.md` → `testing.md`
- **Going to production:** `logging.md` → `configuration.md` → `deployment.md`

## How to use

Read individual rule files for detailed explanations and code examples:

- [rules/plugins.md](rules/plugins.md) - Plugin development and encapsulation
- [rules/routes.md](rules/routes.md) - Route organization and handlers
- [rules/schemas.md](rules/schemas.md) - JSON Schema validation
- [rules/error-handling.md](rules/error-handling.md) - Error handling patterns
- [rules/hooks.md](rules/hooks.md) - Hooks and request lifecycle
- [rules/authentication.md](rules/authentication.md) - Authentication and authorization
- [rules/testing.md](rules/testing.md) - Testing with inject()
- [rules/performance.md](rules/performance.md) - Performance optimization
- [rules/logging.md](rules/logging.md) - Logging with Pino
- [rules/typescript.md](rules/typescript.md) - TypeScript integration
- [rules/decorators.md](rules/decorators.md) - Decorators and extensions
- [rules/content-type.md](rules/content-type.md) - Content type parsing
- [rules/serialization.md](rules/serialization.md) - Response serialization
- [rules/cors-security.md](rules/cors-security.md) - CORS and security headers
- [rules/websockets.md](rules/websockets.md) - WebSocket support
- [rules/database.md](rules/database.md) - Database integration patterns
- [rules/configuration.md](rules/configuration.md) - Application configuration
- [rules/deployment.md](rules/deployment.md) - Production deployment
- [rules/http-proxy.md](rules/http-proxy.md) - HTTP proxying and reply.from()

## Core Principles

- **Encapsulation**: Fastify's plugin system provides automatic encapsulation
- **Schema-first**: Define schemas for validation and serialization
- **Performance**: Fastify is optimized for speed; use its features correctly
- **Async/await**: All handlers and hooks support async functions
- **Minimal dependencies**: Prefer Fastify's built-in features and official plugins
tile.json
{
  "name": "mcollina/fastify-best-practices",
  "version": "0.1.0",
  "private": false,
  "summary": "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.",
  "skills": {
    "fastify-best-practices": {
      "path": "SKILL.md"
    }
  }
}