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: Recommended Reading Order for Common Scenarios - New to Fastify? Start with → → - Adding authentication: → → - Improving performance: → → - Setting up testing: → - Going to production:…

, // Only letters and spaces\n },\n },\n required: ['email', 'name'],\n additionalProperties: false,\n },\n },\n}, handler);\n```\n\n## IP Filtering\n\nRestrict access by IP:\n\n```typescript\nconst allowedIps = new Set([\n '192.168.1.0/24',\n '10.0.0.0/8',\n]);\n\napp.addHook('onRequest', async (request, reply) => {\n if (request.url.startsWith('/admin')) {\n const clientIp = request.ip;\n\n if (!isIpAllowed(clientIp, allowedIps)) {\n reply.code(403).send({ error: 'Forbidden' });\n }\n }\n});\n\nfunction isIpAllowed(ip: string, allowed: Set\u003cstring>): boolean {\n // Implement IP/CIDR matching\n for (const range of allowed) {\n if (ipInRange(ip, range)) return true;\n }\n return false;\n}\n```\n\n## Trust Proxy\n\nConfigure for reverse proxy environments:\n\n```typescript\nconst app = Fastify({\n trustProxy: true, // Trust X-Forwarded-* headers\n});\n\n// Or specific proxy configuration\nconst app = Fastify({\n trustProxy: ['127.0.0.1', '10.0.0.0/8'],\n});\n\n// Now request.ip returns the real client IP\napp.get('/ip', async (request) => {\n return {\n ip: request.ip,\n ips: request.ips, // Array of all IPs in chain\n };\n});\n```\n\n## HTTPS Redirect\n\nForce HTTPS in production:\n\n```typescript\napp.addHook('onRequest', async (request, reply) => {\n if (\n process.env.NODE_ENV === 'production' &&\n request.headers['x-forwarded-proto'] !== 'https'\n ) {\n const httpsUrl = `https://${request.hostname}${request.url}`;\n reply.redirect(301, httpsUrl);\n }\n});\n```\n\n## Security Best Practices Summary\n\n```typescript\nimport Fastify from 'fastify';\nimport cors from '@fastify/cors';\nimport helmet from '@fastify/helmet';\nimport rateLimit from '@fastify/rate-limit';\n\nconst app = Fastify({\n trustProxy: true,\n bodyLimit: 1048576, // 1MB max body\n});\n\n// Security plugins\napp.register(helmet);\napp.register(cors, {\n origin: process.env.ALLOWED_ORIGINS?.split(','),\n credentials: true,\n});\napp.register(rateLimit, {\n max: 100,\n timeWindow: '1 minute',\n});\n\n// Validate all input with schemas\n// Never expose internal errors in production\n// Use parameterized queries for database\n// Keep dependencies updated\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9710,"content_sha256":"3599c3583fefb93462f7f0532f39a8e5520cc720e1a62fe6950414b205a62d75"},{"filename":"rules/database.md","content":"---\nname: database\ndescription: Database integration with Fastify using official adapters\nmetadata:\n tags: database, postgres, mysql, mongodb, redis, sql\n---\n\n# Database Integration\n\n## Contents\n\n- [Use Official Fastify Database Adapters](#use-official-fastify-database-adapters)\n- [PostgreSQL with @fastify/postgres](#postgresql-with-fastifypostgres)\n- [MySQL with @fastify/mysql](#mysql-with-fastifymysql)\n- [MongoDB with @fastify/mongodb](#mongodb-with-fastifymongodb)\n- [Redis with @fastify/redis](#redis-with-fastifyredis)\n- [Database as Plugin](#database-as-plugin)\n- [Repository Pattern](#repository-pattern)\n- [Testing with Database](#testing-with-database)\n- [Connection Pool Configuration](#connection-pool-configuration)\n\n## Use Official Fastify Database Adapters\n\nAlways use the official Fastify database plugins from the `@fastify` organization. They provide proper connection pooling, encapsulation, and integration with Fastify's lifecycle.\n\n## PostgreSQL with @fastify/postgres\n\n```typescript\nimport Fastify from 'fastify';\nimport fastifyPostgres from '@fastify/postgres';\n\nconst app = Fastify({ logger: true });\n\napp.register(fastifyPostgres, {\n connectionString: process.env.DATABASE_URL,\n});\n\n// Use in routes\napp.get('/users', async (request) => {\n const client = await app.pg.connect();\n try {\n const { rows } = await client.query('SELECT * FROM users');\n return rows;\n } finally {\n client.release();\n }\n});\n\n// Or use the pool directly for simple queries\napp.get('/users/:id', async (request) => {\n const { id } = request.params;\n const { rows } = await app.pg.query(\n 'SELECT * FROM users WHERE id = $1',\n [id],\n );\n return rows[0];\n});\n\n// Transactions\napp.post('/transfer', async (request) => {\n const { fromId, toId, amount } = request.body;\n const client = await app.pg.connect();\n\n try {\n await client.query('BEGIN');\n await client.query(\n 'UPDATE accounts SET balance = balance - $1 WHERE id = $2',\n [amount, fromId],\n );\n await client.query(\n 'UPDATE accounts SET balance = balance + $1 WHERE id = $2',\n [amount, toId],\n );\n await client.query('COMMIT');\n return { success: true };\n } catch (error) {\n await client.query('ROLLBACK');\n throw error;\n } finally {\n client.release();\n }\n});\n```\n\n## MySQL with @fastify/mysql\n\n```typescript\nimport Fastify from 'fastify';\nimport fastifyMysql from '@fastify/mysql';\n\nconst app = Fastify({ logger: true });\n\napp.register(fastifyMysql, {\n promise: true,\n connectionString: process.env.MYSQL_URL,\n});\n\napp.get('/users', async (request) => {\n const connection = await app.mysql.getConnection();\n try {\n const [rows] = await connection.query('SELECT * FROM users');\n return rows;\n } finally {\n connection.release();\n }\n});\n```\n\n## MongoDB with @fastify/mongodb\n\n```typescript\nimport Fastify from 'fastify';\nimport fastifyMongo from '@fastify/mongodb';\n\nconst app = Fastify({ logger: true });\n\napp.register(fastifyMongo, {\n url: process.env.MONGODB_URL,\n});\n\napp.get('/users', async (request) => {\n const users = await app.mongo.db\n .collection('users')\n .find({})\n .toArray();\n return users;\n});\n\napp.get('/users/:id', async (request) => {\n const { id } = request.params;\n const user = await app.mongo.db\n .collection('users')\n .findOne({ _id: new app.mongo.ObjectId(id) });\n return user;\n});\n\napp.post('/users', async (request) => {\n const result = await app.mongo.db\n .collection('users')\n .insertOne(request.body);\n return { id: result.insertedId };\n});\n```\n\n## Redis with @fastify/redis\n\n```typescript\nimport Fastify from 'fastify';\nimport fastifyRedis from '@fastify/redis';\n\nconst app = Fastify({ logger: true });\n\napp.register(fastifyRedis, {\n url: process.env.REDIS_URL,\n});\n\n// Caching example\napp.get('/data/:key', async (request) => {\n const { key } = request.params;\n\n // Try cache first\n const cached = await app.redis.get(`cache:${key}`);\n if (cached) {\n return JSON.parse(cached);\n }\n\n // Fetch from database\n const data = await fetchFromDatabase(key);\n\n // Cache for 5 minutes\n await app.redis.setex(`cache:${key}`, 300, JSON.stringify(data));\n\n return data;\n});\n```\n\n## Database as Plugin\n\nEncapsulate database access in a plugin:\n\n```typescript\n// plugins/database.ts\nimport fp from 'fastify-plugin';\nimport fastifyPostgres from '@fastify/postgres';\n\nexport default fp(async function databasePlugin(fastify) {\n await fastify.register(fastifyPostgres, {\n connectionString: fastify.config.DATABASE_URL,\n });\n\n // Add health check\n fastify.decorate('checkDatabaseHealth', async () => {\n try {\n await fastify.pg.query('SELECT 1');\n return true;\n } catch {\n return false;\n }\n });\n}, {\n name: 'database',\n dependencies: ['config'],\n});\n```\n\n## Repository Pattern\n\nAbstract database access with repositories:\n\n```typescript\n// repositories/user.repository.ts\nimport type { FastifyInstance } from 'fastify';\n\nexport interface User {\n id: string;\n email: string;\n name: string;\n}\n\nexport function createUserRepository(app: FastifyInstance) {\n return {\n async findById(id: string): Promise\u003cUser | null> {\n const { rows } = await app.pg.query(\n 'SELECT * FROM users WHERE id = $1',\n [id],\n );\n return rows[0] || null;\n },\n\n async findByEmail(email: string): Promise\u003cUser | null> {\n const { rows } = await app.pg.query(\n 'SELECT * FROM users WHERE email = $1',\n [email],\n );\n return rows[0] || null;\n },\n\n async create(data: Omit\u003cUser, 'id'>): Promise\u003cUser> {\n const { rows } = await app.pg.query(\n 'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',\n [data.email, data.name],\n );\n return rows[0];\n },\n\n async update(id: string, data: Partial\u003cUser>): Promise\u003cUser | null> {\n const fields = Object.keys(data);\n const values = Object.values(data);\n const setClause = fields\n .map((f, i) => `${f} = ${i + 2}`)\n .join(', ');\n\n const { rows } = await app.pg.query(\n `UPDATE users SET ${setClause} WHERE id = $1 RETURNING *`,\n [id, ...values],\n );\n return rows[0] || null;\n },\n\n async delete(id: string): Promise\u003cboolean> {\n const { rowCount } = await app.pg.query(\n 'DELETE FROM users WHERE id = $1',\n [id],\n );\n return rowCount > 0;\n },\n };\n}\n\n// Usage in plugin\nimport fp from 'fastify-plugin';\nimport { createUserRepository } from './repositories/user.repository.js';\n\nexport default fp(async function repositoriesPlugin(fastify) {\n fastify.decorate('repositories', {\n users: createUserRepository(fastify),\n });\n}, {\n name: 'repositories',\n dependencies: ['database'],\n});\n```\n\n## Testing with Database\n\nUse transactions for test isolation:\n\n```typescript\nimport { describe, it, beforeEach, afterEach } from 'node:test';\nimport { build } from './app.js';\n\ndescribe('User API', () => {\n let app;\n let client;\n\n beforeEach(async () => {\n app = await build();\n client = await app.pg.connect();\n await client.query('BEGIN');\n });\n\n afterEach(async () => {\n await client.query('ROLLBACK');\n client.release();\n await app.close();\n });\n\n it('should create a user', async (t) => {\n const response = await app.inject({\n method: 'POST',\n url: '/users',\n payload: { email: '[email protected]', name: 'Test' },\n });\n\n t.assert.equal(response.statusCode, 201);\n });\n});\n```\n\n## Connection Pool Configuration\n\nConfigure connection pools appropriately:\n\n```typescript\napp.register(fastifyPostgres, {\n connectionString: process.env.DATABASE_URL,\n // Pool configuration\n max: 20, // Maximum pool size\n idleTimeoutMillis: 30000, // Close idle clients after 30s\n connectionTimeoutMillis: 5000, // Timeout for new connections\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7863,"content_sha256":"826b8fa648f95497a47f14eb701ea870fc4249bd61e6159b78da606ce0fd4d86"},{"filename":"rules/decorators.md","content":"---\nname: decorators\ndescription: Decorators and request/reply extensions in Fastify\nmetadata:\n tags: decorators, extensions, customization, utilities\n---\n\n# Decorators and Extensions\n\n## Contents\n\n- [Understanding Decorators](#understanding-decorators)\n- [Decorator Types](#decorator-types)\n- [TypeScript Declaration Merging](#typescript-declaration-merging)\n- [Decorator Initialization](#decorator-initialization)\n- [Dependency Injection with Decorators](#dependency-injection-with-decorators)\n- [Request Context Pattern](#request-context-pattern)\n- [Reply Helpers](#reply-helpers)\n- [Checking Decorators](#checking-decorators)\n- [Decorator Encapsulation](#decorator-encapsulation)\n- [Functional Decorators](#functional-decorators)\n- [Async Decorator Initialization](#async-decorator-initialization)\n\n## Understanding Decorators\n\nDecorators add custom properties and methods to Fastify instances, requests, and replies:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify();\n\n// Decorate the Fastify instance\napp.decorate('utility', {\n formatDate: (date: Date) => date.toISOString(),\n generateId: () => crypto.randomUUID(),\n});\n\n// Use in routes\napp.get('/example', async function (request, reply) {\n const id = this.utility.generateId();\n return { id, timestamp: this.utility.formatDate(new Date()) };\n});\n```\n\n## Decorator Types\n\nThree types of decorators for different contexts:\n\n```typescript\n// Instance decorator - available on fastify instance\napp.decorate('config', { apiVersion: '1.0.0' });\napp.decorate('db', databaseConnection);\napp.decorate('cache', cacheClient);\n\n// Request decorator - available on each request\napp.decorateRequest('user', null); // Object property\napp.decorateRequest('startTime', 0); // Primitive\napp.decorateRequest('getData', function() { // Method\n return this.body;\n});\n\n// Reply decorator - available on each reply\napp.decorateReply('sendError', function(code: number, message: string) {\n return this.code(code).send({ error: message });\n});\napp.decorateReply('success', function(data: unknown) {\n return this.send({ success: true, data });\n});\n```\n\n## TypeScript Declaration Merging\n\nExtend Fastify types for type safety:\n\n```typescript\n// Declare custom properties\ndeclare module 'fastify' {\n interface FastifyInstance {\n config: {\n apiVersion: string;\n environment: string;\n };\n db: DatabaseClient;\n cache: CacheClient;\n }\n\n interface FastifyRequest {\n user: {\n id: string;\n email: string;\n roles: string[];\n } | null;\n startTime: number;\n requestId: string;\n }\n\n interface FastifyReply {\n sendError: (code: number, message: string) => void;\n success: (data: unknown) => void;\n }\n}\n\n// Register decorators\napp.decorate('config', {\n apiVersion: '1.0.0',\n environment: process.env.NODE_ENV,\n});\n\napp.decorateRequest('user', null);\napp.decorateRequest('startTime', 0);\n\napp.decorateReply('sendError', function (code: number, message: string) {\n this.code(code).send({ error: message });\n});\n```\n\n## Decorator Initialization\n\nInitialize request/reply decorators in hooks:\n\n```typescript\n// Decorators with primitive defaults are copied\napp.decorateRequest('startTime', 0);\n\n// Initialize in hook\napp.addHook('onRequest', async (request) => {\n request.startTime = Date.now();\n});\n\n// Object decorators need getter pattern for proper initialization\napp.decorateRequest('context', null);\n\napp.addHook('onRequest', async (request) => {\n request.context = {\n traceId: request.headers['x-trace-id'] || crypto.randomUUID(),\n clientIp: request.ip,\n userAgent: request.headers['user-agent'],\n };\n});\n```\n\n## Dependency Injection with Decorators\n\nUse decorators for dependency injection:\n\n```typescript\nimport fp from 'fastify-plugin';\n\n// Database plugin\nexport default fp(async function databasePlugin(fastify, options) {\n const db = await createDatabaseConnection(options.connectionString);\n\n fastify.decorate('db', db);\n\n fastify.addHook('onClose', async () => {\n await db.close();\n });\n});\n\n// User service plugin\nexport default fp(async function userServicePlugin(fastify) {\n // Depends on db decorator\n if (!fastify.hasDecorator('db')) {\n throw new Error('Database plugin must be registered first');\n }\n\n const userService = {\n findById: (id: string) => fastify.db.query('SELECT * FROM users WHERE id = $1', [id]),\n create: (data: CreateUserInput) => fastify.db.query(\n 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',\n [data.name, data.email]\n ),\n };\n\n fastify.decorate('userService', userService);\n}, {\n dependencies: ['database-plugin'],\n});\n\n// Use in routes\napp.get('/users/:id', async function (request) {\n const user = await this.userService.findById(request.params.id);\n return user;\n});\n```\n\n## Request Context Pattern\n\nBuild rich request context:\n\n```typescript\ninterface RequestContext {\n traceId: string;\n user: User | null;\n permissions: Set\u003cstring>;\n startTime: number;\n metadata: Map\u003cstring, unknown>;\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n ctx: RequestContext;\n }\n}\n\napp.decorateRequest('ctx', null);\n\napp.addHook('onRequest', async (request) => {\n request.ctx = {\n traceId: request.headers['x-trace-id']?.toString() || crypto.randomUUID(),\n user: null,\n permissions: new Set(),\n startTime: Date.now(),\n metadata: new Map(),\n };\n});\n\n// Auth hook populates user\napp.addHook('preHandler', async (request) => {\n const token = request.headers.authorization;\n if (token) {\n const user = await verifyToken(token);\n request.ctx.user = user;\n request.ctx.permissions = new Set(user.permissions);\n }\n});\n\n// Use in handlers\napp.get('/profile', async (request, reply) => {\n if (!request.ctx.user) {\n return reply.code(401).send({ error: 'Unauthorized' });\n }\n\n if (!request.ctx.permissions.has('read:profile')) {\n return reply.code(403).send({ error: 'Forbidden' });\n }\n\n return request.ctx.user;\n});\n```\n\n## Reply Helpers\n\nCreate consistent response methods:\n\n```typescript\ndeclare module 'fastify' {\n interface FastifyReply {\n ok: (data?: unknown) => void;\n created: (data: unknown) => void;\n noContent: () => void;\n badRequest: (message: string, details?: unknown) => void;\n unauthorized: (message?: string) => void;\n forbidden: (message?: string) => void;\n notFound: (resource?: string) => void;\n conflict: (message: string) => void;\n serverError: (message?: string) => void;\n }\n}\n\napp.decorateReply('ok', function (data?: unknown) {\n this.code(200).send(data ?? { success: true });\n});\n\napp.decorateReply('created', function (data: unknown) {\n this.code(201).send(data);\n});\n\napp.decorateReply('noContent', function () {\n this.code(204).send();\n});\n\napp.decorateReply('badRequest', function (message: string, details?: unknown) {\n this.code(400).send({\n statusCode: 400,\n error: 'Bad Request',\n message,\n details,\n });\n});\n\napp.decorateReply('unauthorized', function (message = 'Authentication required') {\n this.code(401).send({\n statusCode: 401,\n error: 'Unauthorized',\n message,\n });\n});\n\napp.decorateReply('notFound', function (resource = 'Resource') {\n this.code(404).send({\n statusCode: 404,\n error: 'Not Found',\n message: `${resource} not found`,\n });\n});\n\n// Usage\napp.get('/users/:id', async (request, reply) => {\n const user = await db.users.findById(request.params.id);\n if (!user) {\n return reply.notFound('User');\n }\n return reply.ok(user);\n});\n\napp.post('/users', async (request, reply) => {\n const user = await db.users.create(request.body);\n return reply.created(user);\n});\n```\n\n## Checking Decorators\n\nCheck if decorators exist before using:\n\n```typescript\n// Check at registration time\napp.register(async function (fastify) {\n if (!fastify.hasDecorator('db')) {\n throw new Error('Database decorator required');\n }\n\n if (!fastify.hasRequestDecorator('user')) {\n throw new Error('User request decorator required');\n }\n\n if (!fastify.hasReplyDecorator('sendError')) {\n throw new Error('sendError reply decorator required');\n }\n\n // Safe to use decorators\n});\n```\n\n## Decorator Encapsulation\n\nDecorators respect encapsulation by default:\n\n```typescript\napp.register(async function pluginA(fastify) {\n fastify.decorate('pluginAUtil', () => 'A');\n\n fastify.get('/a', async function () {\n return this.pluginAUtil(); // Works\n });\n});\n\napp.register(async function pluginB(fastify) {\n // this.pluginAUtil is NOT available here (encapsulated)\n\n fastify.get('/b', async function () {\n // this.pluginAUtil() would be undefined\n });\n});\n```\n\nUse `fastify-plugin` to share decorators:\n\n```typescript\nimport fp from 'fastify-plugin';\n\nexport default fp(async function sharedDecorator(fastify) {\n fastify.decorate('sharedUtil', () => 'shared');\n});\n\n// Now available to parent and sibling plugins\n```\n\n## Functional Decorators\n\nCreate decorators that return functions:\n\n```typescript\ndeclare module 'fastify' {\n interface FastifyInstance {\n createValidator: \u003cT>(schema: object) => (data: unknown) => T;\n createRateLimiter: (options: RateLimitOptions) => RateLimiter;\n }\n}\n\napp.decorate('createValidator', function \u003cT>(schema: object) {\n const validate = ajv.compile(schema);\n return (data: unknown): T => {\n if (!validate(data)) {\n throw new ValidationError(validate.errors);\n }\n return data as T;\n };\n});\n\n// Usage\nconst validateUser = app.createValidator\u003cUser>(userSchema);\n\napp.post('/users', async (request) => {\n const user = validateUser(request.body);\n return db.users.create(user);\n});\n```\n\n## Async Decorator Initialization\n\nHandle async initialization properly:\n\n```typescript\nimport fp from 'fastify-plugin';\n\nexport default fp(async function asyncPlugin(fastify) {\n // Async initialization\n const connection = await createAsyncConnection();\n const cache = await initializeCache();\n\n fastify.decorate('asyncService', {\n connection,\n cache,\n query: async (sql: string) => connection.query(sql),\n });\n\n fastify.addHook('onClose', async () => {\n await connection.close();\n await cache.disconnect();\n });\n});\n\n// Plugin is fully initialized before routes execute\napp.get('/data', async function () {\n return this.asyncService.query('SELECT * FROM data');\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10316,"content_sha256":"ffc34ee62a047b64db064993510d0ef5dbe8201b3f3c925bfcab17e04da615d9"},{"filename":"rules/deployment.md","content":"---\nname: deployment\ndescription: Production deployment for Fastify applications\nmetadata:\n tags: deployment, production, docker, kubernetes, scaling\n---\n\n# Production Deployment\n\n## Contents\n\n- [Graceful Shutdown with close-with-grace](#graceful-shutdown-with-close-with-grace)\n- [Health Check Endpoints](#health-check-endpoints)\n- [Docker Configuration](#docker-configuration)\n- [Kubernetes Deployment](#kubernetes-deployment)\n- [Production Logger Configuration](#production-logger-configuration)\n- [Request Timeouts](#request-timeouts)\n- [Trust Proxy Settings](#trust-proxy-settings)\n- [Static File Serving](#static-file-serving)\n- [Compression](#compression)\n- [Metrics and Monitoring](#metrics-and-monitoring)\n- [Zero-Downtime Deployments](#zero-downtime-deployments)\n\n## Graceful Shutdown with close-with-grace\n\nUse `close-with-grace` for proper shutdown handling:\n\n```typescript\nimport Fastify from 'fastify';\nimport closeWithGrace from 'close-with-grace';\n\nconst app = Fastify({ logger: true });\n\n// Register plugins and routes\nawait app.register(import('./plugins/index.js'));\nawait app.register(import('./routes/index.js'));\n\n// Graceful shutdown handler\ncloseWithGrace({ delay: 10000 }, async ({ signal, err }) => {\n if (err) {\n app.log.error({ err }, 'Server closing due to error');\n } else {\n app.log.info({ signal }, 'Server closing due to signal');\n }\n\n await app.close();\n});\n\n// Start server\nawait app.listen({\n port: parseInt(process.env.PORT || '3000', 10),\n host: '0.0.0.0',\n});\n\napp.log.info(`Server listening on ${app.server.address()}`);\n```\n\n## Health Check Endpoints\n\nImplement comprehensive health checks:\n\n```typescript\napp.get('/health', async () => {\n return { status: 'ok', timestamp: new Date().toISOString() };\n});\n\napp.get('/health/live', async () => {\n return { status: 'ok' };\n});\n\napp.get('/health/ready', async (request, reply) => {\n const checks = {\n database: false,\n cache: false,\n };\n\n try {\n await app.db`SELECT 1`;\n checks.database = true;\n } catch {\n // Database not ready\n }\n\n try {\n await app.cache.ping();\n checks.cache = true;\n } catch {\n // Cache not ready\n }\n\n const allHealthy = Object.values(checks).every(Boolean);\n\n if (!allHealthy) {\n reply.code(503);\n }\n\n return {\n status: allHealthy ? 'ok' : 'degraded',\n checks,\n timestamp: new Date().toISOString(),\n };\n});\n\n// Detailed health for monitoring\napp.get('/health/details', {\n preHandler: [app.authenticate, app.requireAdmin],\n}, async () => {\n const memory = process.memoryUsage();\n\n return {\n status: 'ok',\n uptime: process.uptime(),\n memory: {\n heapUsed: Math.round(memory.heapUsed / 1024 / 1024),\n heapTotal: Math.round(memory.heapTotal / 1024 / 1024),\n rss: Math.round(memory.rss / 1024 / 1024),\n },\n version: process.env.APP_VERSION,\n nodeVersion: process.version,\n };\n});\n```\n\n## Docker Configuration\n\nCreate an optimized Dockerfile:\n\n```dockerfile\n# Build stage\nFROM node:22-alpine AS builder\n\nWORKDIR /app\n\nCOPY package*.json ./\nRUN npm ci --only=production\n\nCOPY . .\n\n# Production stage\nFROM node:22-alpine\n\nWORKDIR /app\n\n# Run as non-root user\nRUN addgroup -g 1001 -S nodejs && \\\n adduser -S nodejs -u 1001\n\n# Copy from builder\nCOPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules\nCOPY --from=builder --chown=nodejs:nodejs /app/src ./src\nCOPY --from=builder --chown=nodejs:nodejs /app/package.json ./\n\nUSER nodejs\n\nEXPOSE 3000\n\nENV NODE_ENV=production\nENV PORT=3000\n\n# Health check\nHEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \\\n CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1\n\nCMD [\"node\", \"src/app.ts\"]\n```\n\n```yaml\n# docker-compose.yml\nservices:\n api:\n build: .\n ports:\n - \"3000:3000\"\n environment:\n - NODE_ENV=production\n - DATABASE_URL=postgres://user:pass@db:5432/app\n - JWT_SECRET=${JWT_SECRET}\n depends_on:\n db:\n condition: service_healthy\n restart: unless-stopped\n\n db:\n image: postgres:16-alpine\n environment:\n - POSTGRES_USER=user\n - POSTGRES_PASSWORD=pass\n - POSTGRES_DB=app\n volumes:\n - pgdata:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U user -d app\"]\n interval: 5s\n timeout: 5s\n retries: 5\n\nvolumes:\n pgdata:\n```\n\n## Kubernetes Deployment\n\nDeploy to Kubernetes:\n\n```yaml\n# deployment.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: fastify-api\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: fastify-api\n template:\n metadata:\n labels:\n app: fastify-api\n spec:\n containers:\n - name: api\n image: my-registry/fastify-api:latest\n ports:\n - containerPort: 3000\n env:\n - name: NODE_ENV\n value: \"production\"\n - name: DATABASE_URL\n valueFrom:\n secretKeyRef:\n name: api-secrets\n key: database-url\n resources:\n requests:\n memory: \"256Mi\"\n cpu: \"100m\"\n limits:\n memory: \"512Mi\"\n cpu: \"500m\"\n livenessProbe:\n httpGet:\n path: /health/live\n port: 3000\n initialDelaySeconds: 5\n periodSeconds: 10\n readinessProbe:\n httpGet:\n path: /health/ready\n port: 3000\n initialDelaySeconds: 5\n periodSeconds: 5\n lifecycle:\n preStop:\n exec:\n command: [\"/bin/sh\", \"-c\", \"sleep 5\"]\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: fastify-api\nspec:\n selector:\n app: fastify-api\n ports:\n - port: 80\n targetPort: 3000\n type: ClusterIP\n```\n\n## Production Logger Configuration\n\nConfigure logging for production:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n logger: {\n level: process.env.LOG_LEVEL || 'info',\n // JSON output for log aggregation\n formatters: {\n level: (label) => ({ level: label }),\n bindings: (bindings) => ({\n pid: bindings.pid,\n hostname: bindings.hostname,\n service: 'fastify-api',\n version: process.env.APP_VERSION,\n }),\n },\n timestamp: () => `,\"time\":\"${new Date().toISOString()}\"`,\n // Redact sensitive data\n redact: {\n paths: [\n 'req.headers.authorization',\n 'req.headers.cookie',\n '*.password',\n '*.token',\n '*.secret',\n ],\n censor: '[REDACTED]',\n },\n },\n});\n```\n\n## Request Timeouts\n\nConfigure appropriate timeouts:\n\n```typescript\nconst app = Fastify({\n connectionTimeout: 30000, // 30s connection timeout\n keepAliveTimeout: 72000, // 72s keep-alive (longer than ALB 60s)\n requestTimeout: 30000, // 30s request timeout\n bodyLimit: 1048576, // 1MB body limit\n});\n\n// Per-route timeout\napp.get('/long-operation', {\n config: {\n timeout: 60000, // 60s for this route\n },\n}, longOperationHandler);\n```\n\n## Trust Proxy Settings\n\nConfigure for load balancers:\n\n```typescript\nconst app = Fastify({\n // Trust first proxy (load balancer)\n trustProxy: true,\n\n // Or trust specific proxies\n trustProxy: ['127.0.0.1', '10.0.0.0/8'],\n\n // Or number of proxies to trust\n trustProxy: 1,\n});\n\n// Now request.ip returns real client IP\n```\n\n## Static File Serving\n\nServe static files efficiently. **Always use `import.meta.dirname` as the base path**, never `process.cwd()`:\n\n```typescript\nimport fastifyStatic from '@fastify/static';\nimport { join } from 'node:path';\n\napp.register(fastifyStatic, {\n root: join(import.meta.dirname, '..', 'public'),\n prefix: '/static/',\n maxAge: '1d',\n immutable: true,\n etag: true,\n lastModified: true,\n});\n```\n\n## Compression\n\nEnable response compression:\n\n```typescript\nimport fastifyCompress from '@fastify/compress';\n\napp.register(fastifyCompress, {\n global: true,\n threshold: 1024, // Only compress > 1KB\n encodings: ['gzip', 'deflate'],\n});\n```\n\n## Metrics and Monitoring\n\nExpose Prometheus metrics:\n\n```typescript\nimport { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client';\n\ncollectDefaultMetrics();\n\nconst httpRequestDuration = new Histogram({\n name: 'http_request_duration_seconds',\n help: 'Duration of HTTP requests in seconds',\n labelNames: ['method', 'route', 'status'],\n buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],\n});\n\nconst httpRequestTotal = new Counter({\n name: 'http_requests_total',\n help: 'Total number of HTTP requests',\n labelNames: ['method', 'route', 'status'],\n});\n\napp.addHook('onResponse', (request, reply, done) => {\n const route = request.routeOptions.url || request.url;\n const labels = {\n method: request.method,\n route,\n status: reply.statusCode,\n };\n\n httpRequestDuration.observe(labels, reply.elapsedTime / 1000);\n httpRequestTotal.inc(labels);\n done();\n});\n\napp.get('/metrics', async (request, reply) => {\n reply.header('Content-Type', register.contentType);\n return register.metrics();\n});\n```\n\n## Zero-Downtime Deployments\n\nSupport rolling updates:\n\n```typescript\nimport closeWithGrace from 'close-with-grace';\n\n// Stop accepting new connections gracefully\ncloseWithGrace({ delay: 30000 }, async ({ signal }) => {\n app.log.info({ signal }, 'Received shutdown signal');\n\n // Stop accepting new connections\n // Existing connections continue to be served\n\n // Wait for in-flight requests (handled by close-with-grace delay)\n await app.close();\n\n app.log.info('Server closed');\n});\n```\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9589,"content_sha256":"db9154c99287180e5b8eb2cf13bad31613b2b4c083bdac5b11641d61795869fb"},{"filename":"rules/error-handling.md","content":"---\nname: error-handling\ndescription: Error handling patterns in Fastify\nmetadata:\n tags: errors, exceptions, error-handler, validation\n---\n\n# Error Handling in Fastify\n\n## Contents\n\n- [Default Error Handler](#default-error-handler)\n- [Custom Error Classes](#custom-error-classes)\n- [Custom Error Handler](#custom-error-handler)\n- [Error Response Schema](#error-response-schema)\n- [Reply Helpers with @fastify/sensible](#reply-helpers-with-fastifysensible)\n- [Async Error Handling](#async-error-handling)\n- [Hook Error Handling](#hook-error-handling)\n- [Not Found Handler](#not-found-handler)\n- [Error Wrapping](#error-wrapping)\n- [Validation Error Customization](#validation-error-customization)\n- [Error Cause Chain](#error-cause-chain)\n- [Plugin-Scoped Error Handlers](#plugin-scoped-error-handlers)\n- [Graceful Error Recovery](#graceful-error-recovery)\n\n## Default Error Handler\n\nFastify has a built-in error handler. Thrown errors automatically become HTTP responses:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({ logger: true });\n\napp.get('/users/:id', async (request) => {\n const user = await findUser(request.params.id);\n if (!user) {\n // Throwing an error with statusCode sets the response status\n const error = new Error('User not found');\n error.statusCode = 404;\n throw error;\n }\n return user;\n});\n```\n\n## Custom Error Classes\n\nUse `@fastify/error` for creating typed errors:\n\n```typescript\nimport createError from '@fastify/error';\n\nconst NotFoundError = createError('NOT_FOUND', '%s not found', 404);\nconst UnauthorizedError = createError('UNAUTHORIZED', 'Authentication required', 401);\nconst ForbiddenError = createError('FORBIDDEN', 'Access denied: %s', 403);\nconst ValidationError = createError('VALIDATION_ERROR', '%s', 400);\nconst ConflictError = createError('CONFLICT', '%s already exists', 409);\n\n// Usage\napp.get('/users/:id', async (request) => {\n const user = await findUser(request.params.id);\n if (!user) {\n throw new NotFoundError('User');\n }\n return user;\n});\n\napp.post('/users', async (request) => {\n const exists = await userExists(request.body.email);\n if (exists) {\n throw new ConflictError('Email');\n }\n return createUser(request.body);\n});\n```\n\n## Custom Error Handler\n\nImplement a centralized error handler:\n\n```typescript\nimport Fastify from 'fastify';\nimport type { FastifyError, FastifyRequest, FastifyReply } from 'fastify';\n\nconst app = Fastify({ logger: true });\n\napp.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {\n // Log the error\n request.log.error({ err: error }, 'Request error');\n\n // Handle validation errors\n if (error.validation) {\n return reply.code(400).send({\n statusCode: 400,\n error: 'Bad Request',\n message: 'Validation failed',\n details: error.validation,\n });\n }\n\n // Handle known errors with status codes\n const statusCode = error.statusCode ?? 500;\n const code = error.code ?? 'INTERNAL_ERROR';\n\n // Don't expose internal error details in production\n const message = statusCode >= 500 && process.env.NODE_ENV === 'production'\n ? 'Internal Server Error'\n : error.message;\n\n return reply.code(statusCode).send({\n statusCode,\n error: code,\n message,\n });\n});\n```\n\n## Error Response Schema\n\nDefine consistent error response schemas:\n\n```typescript\napp.addSchema({\n $id: 'httpError',\n type: 'object',\n properties: {\n statusCode: { type: 'integer' },\n error: { type: 'string' },\n message: { type: 'string' },\n details: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n field: { type: 'string' },\n message: { type: 'string' },\n },\n },\n },\n },\n required: ['statusCode', 'error', 'message'],\n});\n\n// Use in route schemas\napp.get('/users/:id', {\n schema: {\n params: {\n type: 'object',\n properties: { id: { type: 'string' } },\n required: ['id'],\n },\n response: {\n 200: { $ref: 'user#' },\n 404: { $ref: 'httpError#' },\n 500: { $ref: 'httpError#' },\n },\n },\n}, handler);\n```\n\n## Reply Helpers with @fastify/sensible\n\nUse `@fastify/sensible` for standard HTTP errors:\n\n```typescript\nimport fastifySensible from '@fastify/sensible';\n\napp.register(fastifySensible);\n\napp.get('/users/:id', async (request, reply) => {\n const user = await findUser(request.params.id);\n if (!user) {\n return reply.notFound('User not found');\n }\n if (!hasAccess(request.user, user)) {\n return reply.forbidden('You cannot access this user');\n }\n return user;\n});\n\n// Available methods:\n// reply.badRequest(message?)\n// reply.unauthorized(message?)\n// reply.forbidden(message?)\n// reply.notFound(message?)\n// reply.methodNotAllowed(message?)\n// reply.conflict(message?)\n// reply.gone(message?)\n// reply.unprocessableEntity(message?)\n// reply.tooManyRequests(message?)\n// reply.internalServerError(message?)\n// reply.notImplemented(message?)\n// reply.badGateway(message?)\n// reply.serviceUnavailable(message?)\n// reply.gatewayTimeout(message?)\n```\n\n## Async Error Handling\n\nErrors in async handlers are automatically caught:\n\n```typescript\n// Errors are automatically caught and passed to error handler\napp.get('/users', async (request) => {\n const users = await db.users.findAll(); // If this throws, error handler catches it\n return users;\n});\n\n// Explicit error handling for custom logic\napp.get('/users/:id', async (request, reply) => {\n try {\n const user = await db.users.findById(request.params.id);\n if (!user) {\n return reply.code(404).send({ error: 'User not found' });\n }\n return user;\n } catch (error) {\n // Transform database errors\n if (error.code === 'CONNECTION_ERROR') {\n request.log.error({ err: error }, 'Database connection failed');\n return reply.code(503).send({ error: 'Service temporarily unavailable' });\n }\n throw error; // Re-throw for error handler\n }\n});\n```\n\n## Hook Error Handling\n\nErrors in hooks are handled the same way:\n\n```typescript\napp.addHook('onRequest', async (request, reply) => {\n const token = request.headers.authorization;\n if (!token) {\n // This error goes to the error handler\n throw new UnauthorizedError();\n }\n\n try {\n request.user = await verifyToken(token);\n } catch (error) {\n throw new UnauthorizedError();\n }\n});\n\n// Or use reply to send response directly\napp.addHook('onRequest', async (request, reply) => {\n if (!request.headers.authorization) {\n reply.code(401).send({ error: 'Unauthorized' });\n return; // Must return to stop processing\n }\n});\n```\n\n## Not Found Handler\n\nCustomize the 404 response:\n\n```typescript\napp.setNotFoundHandler(async (request, reply) => {\n return reply.code(404).send({\n statusCode: 404,\n error: 'Not Found',\n message: `Route ${request.method} ${request.url} not found`,\n });\n});\n\n// With schema validation\napp.setNotFoundHandler({\n preValidation: async (request, reply) => {\n // Pre-validation hook for 404 handler\n },\n}, async (request, reply) => {\n return reply.code(404).send({ error: 'Not Found' });\n});\n```\n\n## Error Wrapping\n\nWrap external errors with context:\n\n```typescript\nimport createError from '@fastify/error';\n\nconst DatabaseError = createError('DATABASE_ERROR', 'Database operation failed: %s', 500);\nconst ExternalServiceError = createError('EXTERNAL_SERVICE_ERROR', 'External service failed: %s', 502);\n\napp.get('/users/:id', async (request) => {\n try {\n return await db.users.findById(request.params.id);\n } catch (error) {\n throw new DatabaseError(error.message, { cause: error });\n }\n});\n\napp.get('/weather', async (request) => {\n try {\n return await weatherApi.fetch(request.query.city);\n } catch (error) {\n throw new ExternalServiceError(error.message, { cause: error });\n }\n});\n```\n\n## Validation Error Customization\n\nCustomize validation error format:\n\n```typescript\napp.setErrorHandler((error, request, reply) => {\n if (error.validation) {\n const details = error.validation.map((err) => {\n const field = err.instancePath\n ? err.instancePath.slice(1).replace(/\\//g, '.')\n : err.params?.missingProperty || 'unknown';\n\n return {\n field,\n message: err.message,\n value: err.data,\n };\n });\n\n return reply.code(400).send({\n statusCode: 400,\n error: 'Validation Error',\n message: `Invalid ${error.validationContext}: ${details.map(d => d.field).join(', ')}`,\n details,\n });\n }\n\n // Handle other errors...\n throw error;\n});\n```\n\n## Error Cause Chain\n\nPreserve error chains for debugging:\n\n```typescript\napp.get('/complex-operation', async (request) => {\n try {\n await step1();\n } catch (error) {\n const wrapped = new Error('Step 1 failed', { cause: error });\n wrapped.statusCode = 500;\n throw wrapped;\n }\n});\n\n// In error handler, log the full chain\napp.setErrorHandler((error, request, reply) => {\n // Log error with cause chain\n let current = error;\n const chain = [];\n while (current) {\n chain.push({\n message: current.message,\n code: current.code,\n stack: current.stack,\n });\n current = current.cause;\n }\n\n request.log.error({ errorChain: chain }, 'Request failed');\n\n reply.code(error.statusCode || 500).send({\n error: error.message,\n });\n});\n```\n\n## Plugin-Scoped Error Handlers\n\nSet error handlers at the plugin level:\n\n```typescript\napp.register(async function apiRoutes(fastify) {\n // This error handler only applies to routes in this plugin\n fastify.setErrorHandler((error, request, reply) => {\n request.log.error({ err: error }, 'API error');\n\n reply.code(error.statusCode || 500).send({\n error: {\n code: error.code || 'API_ERROR',\n message: error.message,\n },\n });\n });\n\n fastify.get('/data', async () => {\n throw new Error('API-specific error');\n });\n}, { prefix: '/api' });\n```\n\n## Graceful Error Recovery\n\nHandle errors gracefully without crashing:\n\n```typescript\napp.get('/resilient', async (request, reply) => {\n const results = await Promise.allSettled([\n fetchPrimaryData(),\n fetchSecondaryData(),\n fetchOptionalData(),\n ]);\n\n const [primary, secondary, optional] = results;\n\n if (primary.status === 'rejected') {\n // Primary data is required\n throw new Error('Primary data unavailable');\n }\n\n return {\n data: primary.value,\n secondary: secondary.status === 'fulfilled' ? secondary.value : null,\n optional: optional.status === 'fulfilled' ? optional.value : null,\n warnings: results\n .filter((r) => r.status === 'rejected')\n .map((r) => r.reason.message),\n };\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10625,"content_sha256":"b54cd93c36fe7626f496c6e70ba1b67389e096c8574398cb5b538d4b2579a133"},{"filename":"rules/hooks.md","content":"---\nname: hooks\ndescription: Hooks and request lifecycle in Fastify\nmetadata:\n tags: hooks, lifecycle, middleware, onRequest, preHandler\n---\n\n# Hooks and Request Lifecycle\n\n## Contents\n\n- [Request Lifecycle Overview](#request-lifecycle-overview)\n- [onRequest Hook](#onrequest-hook)\n- [preParsing Hook](#preparsing-hook)\n- [preValidation Hook](#prevalidation-hook)\n- [preHandler Hook](#prehandler-hook)\n- [preSerialization Hook](#preserialization-hook)\n- [onSend Hook](#onsend-hook)\n- [onResponse Hook](#onresponse-hook)\n- [onError Hook](#onerror-hook)\n- [onTimeout Hook](#ontimeout-hook)\n- [onRequestAbort Hook](#onrequestabort-hook)\n- [Application Lifecycle Hooks](#application-lifecycle-hooks)\n- [Scoped Hooks](#scoped-hooks)\n- [Hook Execution Order](#hook-execution-order)\n- [Stopping Hook Execution](#stopping-hook-execution)\n- [Route-Level Hooks](#route-level-hooks)\n- [Async Hook Patterns](#async-hook-patterns)\n\n## Request Lifecycle Overview\n\nFastify executes hooks in a specific order:\n\n```\nIncoming Request\n |\n onRequest\n |\n preParsing\n |\n preValidation\n |\n preHandler\n |\n Handler\n |\n preSerialization\n |\n onSend\n |\n onResponse\n```\n\n## onRequest Hook\n\nFirst hook to execute, before body parsing. Use for authentication, request ID setup:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify();\n\n// Global onRequest hook\napp.addHook('onRequest', async (request, reply) => {\n request.startTime = Date.now();\n request.log.info({ url: request.url, method: request.method }, 'Request started');\n});\n\n// Authentication check\napp.addHook('onRequest', async (request, reply) => {\n // Skip auth for public routes\n if (request.url.startsWith('/public')) {\n return;\n }\n\n const token = request.headers.authorization?.replace('Bearer ', '');\n if (!token) {\n reply.code(401).send({ error: 'Unauthorized' });\n return; // Stop processing\n }\n\n try {\n request.user = await verifyToken(token);\n } catch {\n reply.code(401).send({ error: 'Invalid token' });\n }\n});\n```\n\n## preParsing Hook\n\nExecute before body parsing. Can modify the payload stream:\n\n```typescript\napp.addHook('preParsing', async (request, reply, payload) => {\n // Log raw payload size\n request.log.debug({ contentLength: request.headers['content-length'] }, 'Parsing body');\n\n // Return modified payload stream if needed\n return payload;\n});\n\n// Decompress incoming data\napp.addHook('preParsing', async (request, reply, payload) => {\n if (request.headers['content-encoding'] === 'gzip') {\n return payload.pipe(zlib.createGunzip());\n }\n return payload;\n});\n```\n\n## preValidation Hook\n\nExecute after parsing, before schema validation:\n\n```typescript\napp.addHook('preValidation', async (request, reply) => {\n // Modify body before validation\n if (request.body && typeof request.body === 'object') {\n // Normalize data\n request.body.email = request.body.email?.toLowerCase().trim();\n }\n});\n\n// Rate limiting check\napp.addHook('preValidation', async (request, reply) => {\n const key = request.ip;\n const count = await redis.incr(`ratelimit:${key}`);\n\n if (count === 1) {\n await redis.expire(`ratelimit:${key}`, 60);\n }\n\n if (count > 100) {\n reply.code(429).send({ error: 'Too many requests' });\n }\n});\n```\n\n## preHandler Hook\n\nMost common hook, execute after validation, before handler:\n\n```typescript\n// Authorization check\napp.addHook('preHandler', async (request, reply) => {\n const { userId } = request.params as { userId: string };\n\n if (request.user.id !== userId && !request.user.isAdmin) {\n reply.code(403).send({ error: 'Forbidden' });\n }\n});\n\n// Load related data\napp.addHook('preHandler', async (request, reply) => {\n if (request.params?.projectId) {\n request.project = await db.projects.findById(request.params.projectId);\n if (!request.project) {\n reply.code(404).send({ error: 'Project not found' });\n }\n }\n});\n\n// Transaction wrapper\napp.addHook('preHandler', async (request) => {\n request.transaction = await db.beginTransaction();\n});\n\napp.addHook('onResponse', async (request) => {\n if (request.transaction) {\n await request.transaction.commit();\n }\n});\n\napp.addHook('onError', async (request, reply, error) => {\n if (request.transaction) {\n await request.transaction.rollback();\n }\n});\n```\n\n## preSerialization Hook\n\nModify payload before serialization:\n\n```typescript\napp.addHook('preSerialization', async (request, reply, payload) => {\n // Add metadata to all responses\n if (payload && typeof payload === 'object') {\n return {\n ...payload,\n _meta: {\n requestId: request.id,\n timestamp: new Date().toISOString(),\n },\n };\n }\n return payload;\n});\n\n// Remove sensitive fields\napp.addHook('preSerialization', async (request, reply, payload) => {\n if (payload?.user?.password) {\n const { password, ...user } = payload.user;\n return { ...payload, user };\n }\n return payload;\n});\n```\n\n## onSend Hook\n\nModify response after serialization:\n\n```typescript\napp.addHook('onSend', async (request, reply, payload) => {\n // Add response headers\n reply.header('X-Response-Time', Date.now() - request.startTime);\n\n // Compress response\n if (payload && payload.length > 1024) {\n const compressed = await gzip(payload);\n reply.header('Content-Encoding', 'gzip');\n return compressed;\n }\n\n return payload;\n});\n\n// Transform JSON string response\napp.addHook('onSend', async (request, reply, payload) => {\n if (reply.getHeader('content-type')?.includes('application/json')) {\n // payload is already a string at this point\n return payload;\n }\n return payload;\n});\n```\n\n## onResponse Hook\n\nExecute after response is sent. Cannot modify response:\n\n```typescript\napp.addHook('onResponse', async (request, reply) => {\n // Log response time\n const responseTime = Date.now() - request.startTime;\n request.log.info({\n method: request.method,\n url: request.url,\n statusCode: reply.statusCode,\n responseTime,\n }, 'Request completed');\n\n // Track metrics\n metrics.histogram('http_request_duration', responseTime, {\n method: request.method,\n route: request.routeOptions.url,\n status: reply.statusCode,\n });\n});\n```\n\n## onError Hook\n\nExecute when an error is thrown:\n\n```typescript\napp.addHook('onError', async (request, reply, error) => {\n // Log error details\n request.log.error({\n err: error,\n url: request.url,\n method: request.method,\n body: request.body,\n }, 'Request error');\n\n // Track error metrics\n metrics.increment('http_errors', {\n error: error.code || 'UNKNOWN',\n route: request.routeOptions.url,\n });\n\n // Cleanup resources\n if (request.tempFile) {\n await fs.unlink(request.tempFile).catch(() => {});\n }\n});\n```\n\n## onTimeout Hook\n\nExecute when request times out:\n\n```typescript\nconst app = Fastify({\n connectionTimeout: 30000, // 30 seconds\n});\n\napp.addHook('onTimeout', async (request, reply) => {\n request.log.warn({\n url: request.url,\n method: request.method,\n }, 'Request timeout');\n\n // Cleanup\n if (request.abortController) {\n request.abortController.abort();\n }\n});\n```\n\n## onRequestAbort Hook\n\nExecute when client closes connection:\n\n```typescript\napp.addHook('onRequestAbort', async (request) => {\n request.log.info('Client aborted request');\n\n // Cancel ongoing operations\n if (request.abortController) {\n request.abortController.abort();\n }\n\n // Cleanup uploaded files\n if (request.uploadedFiles) {\n for (const file of request.uploadedFiles) {\n await fs.unlink(file.path).catch(() => {});\n }\n }\n});\n```\n\n## Application Lifecycle Hooks\n\nHooks that run at application startup/shutdown:\n\n```typescript\n// After all plugins are loaded\napp.addHook('onReady', async function () {\n this.log.info('Server is ready');\n\n // Initialize connections\n await this.db.connect();\n await this.redis.connect();\n\n // Warm caches\n await this.cache.warmup();\n});\n\n// When server is closing\napp.addHook('onClose', async function () {\n this.log.info('Server is closing');\n\n // Cleanup connections\n await this.db.close();\n await this.redis.disconnect();\n});\n\n// After routes are registered\napp.addHook('onRoute', (routeOptions) => {\n console.log(`Route registered: ${routeOptions.method} ${routeOptions.url}`);\n\n // Track all routes\n routes.push({\n method: routeOptions.method,\n url: routeOptions.url,\n schema: routeOptions.schema,\n });\n});\n\n// After plugin is registered\napp.addHook('onRegister', (instance, options) => {\n console.log(`Plugin registered with prefix: ${options.prefix}`);\n});\n```\n\n## Scoped Hooks\n\nHooks are scoped to their encapsulation context:\n\n```typescript\napp.addHook('onRequest', async (request) => {\n // Runs for ALL routes\n request.log.info('Global hook');\n});\n\napp.register(async function adminRoutes(fastify) {\n // Only runs for routes in this plugin\n fastify.addHook('onRequest', async (request, reply) => {\n if (!request.user?.isAdmin) {\n reply.code(403).send({ error: 'Admin only' });\n }\n });\n\n fastify.get('/admin/users', async () => {\n return { users: [] };\n });\n}, { prefix: '/admin' });\n```\n\n## Hook Execution Order\n\nMultiple hooks of the same type execute in registration order:\n\n```typescript\napp.addHook('onRequest', async () => {\n console.log('First');\n});\n\napp.addHook('onRequest', async () => {\n console.log('Second');\n});\n\napp.addHook('onRequest', async () => {\n console.log('Third');\n});\n\n// Output: First, Second, Third\n```\n\n## Stopping Hook Execution\n\nReturn early from hooks to stop processing:\n\n```typescript\napp.addHook('preHandler', async (request, reply) => {\n if (!request.user) {\n // Send response and return to stop further processing\n reply.code(401).send({ error: 'Unauthorized' });\n return;\n }\n // Continue to next hook and handler\n});\n```\n\n## Route-Level Hooks\n\nAdd hooks to specific routes:\n\n```typescript\nconst adminOnlyHook = async (request, reply) => {\n if (!request.user?.isAdmin) {\n reply.code(403).send({ error: 'Forbidden' });\n }\n};\n\napp.get('/admin/settings', {\n preHandler: [adminOnlyHook],\n handler: async (request) => {\n return { settings: {} };\n },\n});\n\n// Multiple hooks\napp.post('/orders', {\n preValidation: [validateApiKey],\n preHandler: [loadUser, checkQuota, logOrder],\n handler: createOrderHandler,\n});\n```\n\n## Async Hook Patterns\n\nAlways use async/await in hooks:\n\n```typescript\n// GOOD - async hook\napp.addHook('preHandler', async (request, reply) => {\n const user = await loadUser(request.headers.authorization);\n request.user = user;\n});\n\n// AVOID - callback style (deprecated)\napp.addHook('preHandler', (request, reply, done) => {\n loadUser(request.headers.authorization)\n .then((user) => {\n request.user = user;\n done();\n })\n .catch(done);\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10801,"content_sha256":"79da189de22300ecf123c9ed06f81f86658bc1d961a0bcd289e31b11bc7918d2"},{"filename":"rules/http-proxy.md","content":"---\nname: http-proxy\ndescription: HTTP proxying and reply.from() in Fastify\nmetadata:\n tags: proxy, gateway, reverse-proxy, microservices\n---\n\n# HTTP Proxy and Reply.from()\n\n## Contents\n\n- [@fastify/http-proxy](#fastifyhttp-proxy)\n- [@fastify/reply-from](#fastifyreply-from)\n- [API Gateway Pattern](#api-gateway-pattern)\n- [Request Body Handling](#request-body-handling)\n- [Error Handling](#error-handling)\n- [WebSocket Proxying](#websocket-proxying)\n- [Timeout Configuration](#timeout-configuration)\n- [Caching Proxied Responses](#caching-proxied-responses)\n\n## @fastify/http-proxy\n\nUse `@fastify/http-proxy` for simple reverse proxy scenarios:\n\n```typescript\nimport Fastify from 'fastify';\nimport httpProxy from '@fastify/http-proxy';\n\nconst app = Fastify({ logger: true });\n\n// Proxy all requests to /api/* to another service\napp.register(httpProxy, {\n upstream: 'http://backend-service:3001',\n prefix: '/api',\n rewritePrefix: '/v1',\n http2: false,\n});\n\n// With authentication\napp.register(httpProxy, {\n upstream: 'http://internal-api:3002',\n prefix: '/internal',\n preHandler: async (request, reply) => {\n // Verify authentication before proxying\n if (!request.headers.authorization) {\n reply.code(401).send({ error: 'Unauthorized' });\n }\n },\n});\n\nawait app.listen({ port: 3000 });\n```\n\n## @fastify/reply-from\n\nFor more control over proxying, use `@fastify/reply-from` with `reply.from()`:\n\n```typescript\nimport Fastify from 'fastify';\nimport replyFrom from '@fastify/reply-from';\n\nconst app = Fastify({ logger: true });\n\napp.register(replyFrom, {\n base: 'http://backend-service:3001',\n http2: false,\n});\n\n// Proxy with request/response manipulation\napp.get('/users/:id', async (request, reply) => {\n const { id } = request.params;\n\n return reply.from(`/api/users/${id}`, {\n // Modify request before forwarding\n rewriteRequestHeaders: (originalReq, headers) => ({\n ...headers,\n 'x-request-id': request.id,\n 'x-forwarded-for': request.ip,\n }),\n // Modify response before sending\n onResponse: (request, reply, res) => {\n reply.header('x-proxy', 'fastify');\n reply.send(res);\n },\n });\n});\n\n// Conditional routing\napp.all('/api/*', async (request, reply) => {\n const upstream = selectUpstream(request);\n\n return reply.from(request.url, {\n base: upstream,\n });\n});\n\nfunction selectUpstream(request) {\n // Route to different backends based on request\n if (request.headers['x-beta']) {\n return 'http://beta-backend:3001';\n }\n return 'http://stable-backend:3001';\n}\n```\n\n## API Gateway Pattern\n\nBuild an API gateway with multiple backends:\n\n```typescript\nimport Fastify from 'fastify';\nimport replyFrom from '@fastify/reply-from';\n\nconst app = Fastify({ logger: true });\n\n// Configure multiple upstreams\nconst services = {\n users: 'http://users-service:3001',\n orders: 'http://orders-service:3002',\n products: 'http://products-service:3003',\n};\n\napp.register(replyFrom);\n\n// Route to user service\napp.register(async function (fastify) {\n fastify.all('/*', async (request, reply) => {\n return reply.from(request.url.replace('/users', ''), {\n base: services.users,\n });\n });\n}, { prefix: '/users' });\n\n// Route to orders service\napp.register(async function (fastify) {\n fastify.all('/*', async (request, reply) => {\n return reply.from(request.url.replace('/orders', ''), {\n base: services.orders,\n });\n });\n}, { prefix: '/orders' });\n\n// Route to products service\napp.register(async function (fastify) {\n fastify.all('/*', async (request, reply) => {\n return reply.from(request.url.replace('/products', ''), {\n base: services.products,\n });\n });\n}, { prefix: '/products' });\n```\n\n## Request Body Handling\n\nHandle request bodies when proxying:\n\n```typescript\napp.post('/api/data', async (request, reply) => {\n return reply.from('/data', {\n body: request.body,\n contentType: request.headers['content-type'],\n });\n});\n\n// Stream large bodies\napp.post('/upload', async (request, reply) => {\n return reply.from('/upload', {\n body: request.raw,\n contentType: request.headers['content-type'],\n });\n});\n```\n\n## Error Handling\n\nHandle upstream errors gracefully:\n\n```typescript\napp.register(replyFrom, {\n base: 'http://backend:3001',\n // Called when upstream returns an error\n onError: (reply, error) => {\n reply.log.error({ err: error }, 'Proxy error');\n reply.code(502).send({\n error: 'Bad Gateway',\n message: 'Upstream service unavailable',\n });\n },\n});\n\n// Custom error handling per route\napp.get('/data', async (request, reply) => {\n try {\n return await reply.from('/data');\n } catch (error) {\n request.log.error({ err: error }, 'Failed to proxy request');\n return reply.code(503).send({\n error: 'Service Unavailable',\n retryAfter: 30,\n });\n }\n});\n```\n\n## WebSocket Proxying\n\nProxy WebSocket connections:\n\n```typescript\nimport Fastify from 'fastify';\nimport httpProxy from '@fastify/http-proxy';\n\nconst app = Fastify({ logger: true });\n\napp.register(httpProxy, {\n upstream: 'http://ws-backend:3001',\n prefix: '/ws',\n websocket: true,\n});\n```\n\n## Timeout Configuration\n\nConfigure proxy timeouts:\n\n```typescript\napp.register(replyFrom, {\n base: 'http://backend:3001',\n http: {\n requestOptions: {\n timeout: 30000, // 30 seconds\n },\n },\n});\n```\n\n## Caching Proxied Responses\n\nAdd caching to proxied responses:\n\n```typescript\nimport { createCache } from 'async-cache-dedupe';\n\nconst cache = createCache({\n ttl: 60,\n storage: { type: 'memory' },\n});\n\ncache.define('proxyGet', async (url: string) => {\n const response = await fetch(`http://backend:3001${url}`);\n return response.json();\n});\n\napp.get('/cached/*', async (request, reply) => {\n const data = await cache.proxyGet(request.url);\n return data;\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5807,"content_sha256":"2449409b0ddfe3784f4bc9de658c9a63cf3b9b266bbbeda090d7ec36305ef4ca"},{"filename":"rules/logging.md","content":"---\nname: logging\ndescription: Logging with Pino in Fastify\nmetadata:\n tags: logging, pino, debugging, observability\n---\n\n# Logging with Pino\n\n## Contents\n\n- [Built-in Pino Integration](#built-in-pino-integration)\n- [Log Levels](#log-levels)\n- [Request-Scoped Logging](#request-scoped-logging)\n- [Structured Logging](#structured-logging)\n- [Logging Configuration by Environment](#logging-configuration-by-environment)\n- [Custom Serializers](#custom-serializers)\n- [Redacting Sensitive Data](#redacting-sensitive-data)\n- [Child Loggers](#child-loggers)\n- [Request Logging Configuration](#request-logging-configuration)\n- [Logging Errors](#logging-errors)\n- [Log Destinations](#log-destinations)\n- [Log Rotation](#log-rotation)\n- [Log Aggregation](#log-aggregation)\n- [Request ID Tracking](#request-id-tracking)\n- [Performance Considerations](#performance-considerations)\n\n## Built-in Pino Integration\n\nFastify uses Pino for high-performance logging:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n logger: true, // Enable default logging\n});\n\n// Or with configuration\nconst app = Fastify({\n logger: {\n level: 'info',\n transport: {\n target: 'pino-pretty',\n options: {\n colorize: true,\n },\n },\n },\n});\n```\n\n## Log Levels\n\nAvailable log levels (in order of severity):\n\n```typescript\napp.log.trace('Detailed debugging');\napp.log.debug('Debugging information');\napp.log.info('General information');\napp.log.warn('Warning messages');\napp.log.error('Error messages');\napp.log.fatal('Fatal errors');\n```\n\n## Request-Scoped Logging\n\nEach request has its own logger with request context:\n\n```typescript\napp.get('/users/:id', async (request) => {\n // Logs include request ID automatically\n request.log.info('Fetching user');\n\n const user = await db.users.findById(request.params.id);\n\n if (!user) {\n request.log.warn({ userId: request.params.id }, 'User not found');\n return { error: 'Not found' };\n }\n\n request.log.info({ userId: user.id }, 'User fetched');\n return user;\n});\n```\n\n## Structured Logging\n\nAlways use structured logging with objects:\n\n```typescript\n// GOOD - structured, searchable\nrequest.log.info({\n action: 'user_created',\n userId: user.id,\n email: user.email,\n}, 'User created successfully');\n\nrequest.log.error({\n err: error,\n userId: request.params.id,\n operation: 'fetch_user',\n}, 'Failed to fetch user');\n\n// BAD - unstructured, hard to parse\nrequest.log.info(`User ${user.id} created with email ${user.email}`);\nrequest.log.error(`Failed to fetch user: ${error.message}`);\n```\n\n## Logging Configuration by Environment\n\n```typescript\nfunction getLoggerConfig() {\n if (process.env.NODE_ENV === 'production') {\n return {\n level: 'info',\n // JSON output for log aggregation\n };\n }\n\n if (process.env.NODE_ENV === 'test') {\n return false; // Disable logging in tests\n }\n\n // Development\n return {\n level: 'debug',\n transport: {\n target: 'pino-pretty',\n options: {\n colorize: true,\n translateTime: 'HH:MM:ss Z',\n ignore: 'pid,hostname',\n },\n },\n };\n}\n\nconst app = Fastify({\n logger: getLoggerConfig(),\n});\n```\n\n## Custom Serializers\n\nCustomize how objects are serialized:\n\n```typescript\nconst app = Fastify({\n logger: {\n level: 'info',\n serializers: {\n // Customize request serialization\n req: (request) => ({\n method: request.method,\n url: request.url,\n headers: {\n host: request.headers.host,\n 'user-agent': request.headers['user-agent'],\n },\n remoteAddress: request.ip,\n }),\n\n // Customize response serialization\n res: (response) => ({\n statusCode: response.statusCode,\n }),\n\n // Custom serializer for users\n user: (user) => ({\n id: user.id,\n email: user.email,\n // Exclude sensitive fields\n }),\n },\n },\n});\n\n// Use custom serializer\nrequest.log.info({ user: request.user }, 'User action');\n```\n\n## Redacting Sensitive Data\n\nPrevent logging sensitive information:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n logger: {\n level: 'info',\n redact: {\n paths: [\n 'req.headers.authorization',\n 'req.headers.cookie',\n 'body.password',\n 'body.creditCard',\n '*.password',\n '*.secret',\n '*.token',\n ],\n censor: '[REDACTED]',\n },\n },\n});\n```\n\n## Child Loggers\n\nCreate child loggers with additional context:\n\n```typescript\napp.addHook('onRequest', async (request) => {\n // Add user context to all logs for this request\n if (request.user) {\n request.log = request.log.child({\n userId: request.user.id,\n userRole: request.user.role,\n });\n }\n});\n\n// Service-level child logger\nconst userService = {\n log: app.log.child({ service: 'UserService' }),\n\n async create(data) {\n this.log.info({ email: data.email }, 'Creating user');\n // ...\n },\n};\n```\n\n## Request Logging Configuration\n\nCustomize automatic request logging:\n\n```typescript\nconst app = Fastify({\n logger: true,\n disableRequestLogging: true, // Disable default request/response logs\n});\n\n// Custom request logging\napp.addHook('onRequest', async (request) => {\n request.log.info({\n method: request.method,\n url: request.url,\n query: request.query,\n }, 'Request received');\n});\n\napp.addHook('onResponse', async (request, reply) => {\n request.log.info({\n statusCode: reply.statusCode,\n responseTime: reply.elapsedTime,\n }, 'Request completed');\n});\n```\n\n## Logging Errors\n\nProperly log errors with stack traces:\n\n```typescript\napp.setErrorHandler((error, request, reply) => {\n // Log error with full details\n request.log.error({\n err: error, // Pino serializes error objects properly\n url: request.url,\n method: request.method,\n body: request.body,\n query: request.query,\n }, 'Request error');\n\n reply.code(error.statusCode || 500).send({\n error: error.message,\n });\n});\n\n// In handlers\napp.get('/data', async (request) => {\n try {\n return await fetchData();\n } catch (error) {\n request.log.error({ err: error }, 'Failed to fetch data');\n throw error;\n }\n});\n```\n\n## Log Destinations\n\nConfigure where logs are sent:\n\n```typescript\nimport { createWriteStream } from 'node:fs';\n\n// File output\nconst app = Fastify({\n logger: {\n level: 'info',\n stream: createWriteStream('./app.log'),\n },\n});\n\n// Multiple destinations with pino.multistream\nimport pino from 'pino';\n\nconst streams = [\n { stream: process.stdout },\n { stream: createWriteStream('./app.log') },\n { level: 'error', stream: createWriteStream('./error.log') },\n];\n\nconst app = Fastify({\n logger: pino({ level: 'info' }, pino.multistream(streams)),\n});\n```\n\n## Log Rotation\n\nUse pino-roll for log rotation:\n\n```bash\nnode app.js | pino-roll --frequency daily --extension .log\n```\n\nOr configure programmatically:\n\n```typescript\nimport { createStream } from 'rotating-file-stream';\n\nconst stream = createStream('app.log', {\n size: '10M', // Rotate every 10MB\n interval: '1d', // Rotate daily\n compress: 'gzip',\n path: './logs',\n});\n\nconst app = Fastify({\n logger: {\n level: 'info',\n stream,\n },\n});\n```\n\n## Log Aggregation\n\nFormat logs for aggregation services:\n\n```typescript\n// For ELK Stack, Datadog, etc. - use default JSON format\nconst app = Fastify({\n logger: {\n level: 'info',\n // Default JSON output works with most log aggregators\n },\n});\n\n// Add service metadata\nconst app = Fastify({\n logger: {\n level: 'info',\n base: {\n service: 'user-api',\n version: process.env.APP_VERSION,\n environment: process.env.NODE_ENV,\n },\n },\n});\n```\n\n## Request ID Tracking\n\nUse request IDs for distributed tracing:\n\n```typescript\nconst app = Fastify({\n logger: true,\n requestIdHeader: 'x-request-id', // Use incoming header\n genReqId: (request) => {\n // Generate ID if not provided\n return request.headers['x-request-id'] || crypto.randomUUID();\n },\n});\n\n// Forward request ID to downstream services\napp.addHook('onRequest', async (request) => {\n request.requestId = request.id;\n});\n\n// Include in outgoing requests\nconst response = await fetch('http://other-service/api', {\n headers: {\n 'x-request-id': request.id,\n },\n});\n```\n\n## Performance Considerations\n\nPino is fast, but consider:\n\n```typescript\n// Avoid string concatenation in log calls\n// BAD\nrequest.log.info('User ' + user.id + ' did ' + action);\n\n// GOOD\nrequest.log.info({ userId: user.id, action }, 'User action');\n\n// Use appropriate log levels\n// Don't log at info level in hot paths\nif (app.log.isLevelEnabled('debug')) {\n request.log.debug({ details: expensiveToCompute() }, 'Debug info');\n}\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8706,"content_sha256":"cf79847834da476af7f169e73eabb78ad1809112de8e1650d25714d6f1cb172c"},{"filename":"rules/performance.md","content":"---\nname: performance\ndescription: Performance optimization for Fastify applications\nmetadata:\n tags: performance, optimization, speed, benchmarking\n---\n\n# Performance Optimization\n\n## Contents\n\n- [Fastify is Fast by Default](#fastify-is-fast-by-default)\n- [Use @fastify/under-pressure for Load Shedding](#use-fastifyunder-pressure-for-load-shedding)\n- [Always Define Response Schemas](#always-define-response-schemas)\n- [Avoid Dynamic Schema Compilation](#avoid-dynamic-schema-compilation)\n- [Use Logger Wisely](#use-logger-wisely)\n- [Connection Pooling](#connection-pooling)\n- [Avoid Blocking the Event Loop](#avoid-blocking-the-event-loop)\n- [Stream Large Responses](#stream-large-responses)\n- [Caching Strategies](#caching-strategies)\n- [Request Coalescing with async-cache-dedupe](#request-coalescing-with-async-cache-dedupe)\n- [Payload Limits](#payload-limits)\n- [Compression](#compression)\n- [Connection Timeouts](#connection-timeouts)\n- [Disable Unnecessary Features](#disable-unnecessary-features)\n- [Benchmarking](#benchmarking)\n- [Profiling](#profiling)\n- [Memory Management](#memory-management)\n\n## Fastify is Fast by Default\n\nFastify is designed for performance. Key optimizations are built-in:\n\n- Fast JSON serialization with `fast-json-stringify`\n- Efficient routing with `find-my-way`\n- Schema-based validation with `ajv` (compiled validators)\n- Low overhead request/response handling\n\n## Use @fastify/under-pressure for Load Shedding\n\nProtect your application from overload with `@fastify/under-pressure`:\n\n```typescript\nimport underPressure from '@fastify/under-pressure';\n\napp.register(underPressure, {\n maxEventLoopDelay: 1000, // Max event loop delay in ms\n maxHeapUsedBytes: 1000000000, // Max heap used (~1GB)\n maxRssBytes: 1500000000, // Max RSS (~1.5GB)\n maxEventLoopUtilization: 0.98, // Max event loop utilization\n pressureHandler: (request, reply, type, value) => {\n reply.code(503).send({\n error: 'Service Unavailable',\n message: `Server under pressure: ${type}`,\n });\n },\n});\n\n// Health check that respects pressure\napp.get('/health', async (request, reply) => {\n return { status: 'ok' };\n});\n```\n\n## Always Define Response Schemas\n\nResponse schemas enable fast-json-stringify, which is significantly faster than JSON.stringify:\n\n```typescript\n// FAST - uses fast-json-stringify\napp.get('/users', {\n schema: {\n response: {\n 200: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n email: { type: 'string' },\n },\n },\n },\n },\n },\n}, async () => {\n return db.users.findAll();\n});\n\n// SLOW - uses JSON.stringify\napp.get('/users-slow', async () => {\n return db.users.findAll();\n});\n```\n\n## Avoid Dynamic Schema Compilation\n\nAdd schemas at startup, not at request time:\n\n```typescript\n// GOOD - schemas compiled at startup\napp.addSchema({ $id: 'user', ... });\n\napp.get('/users', {\n schema: { response: { 200: { $ref: 'user#' } } },\n}, handler);\n\n// BAD - schema compiled per request\napp.get('/users', async (request, reply) => {\n const schema = getSchemaForUser(request.user);\n // This is slow!\n});\n```\n\n## Use Logger Wisely\n\nPino is fast, but excessive logging has overhead:\n\n```typescript\nimport Fastify from 'fastify';\n\n// Set log level via environment variable\nconst app = Fastify({\n logger: {\n level: process.env.LOG_LEVEL || 'info',\n },\n});\n\n// Avoid logging large objects\napp.get('/data', async (request) => {\n // BAD - logs entire payload\n request.log.info({ data: largeObject }, 'Processing');\n\n // GOOD - log only what's needed\n request.log.info({ id: largeObject.id }, 'Processing');\n\n return largeObject;\n});\n```\n\n## Connection Pooling\n\nUse connection pools for databases:\n\n```typescript\nimport postgres from 'postgres';\n\n// Create pool at startup\nconst sql = postgres(process.env.DATABASE_URL, {\n max: 20, // Maximum pool size\n idle_timeout: 20,\n connect_timeout: 10,\n});\n\napp.decorate('db', sql);\n\n// Connections are reused\napp.get('/users', async () => {\n return app.db`SELECT * FROM users LIMIT 100`;\n});\n```\n\n## Avoid Blocking the Event Loop\n\nUse `piscina` for CPU-intensive operations. It provides a robust worker thread pool:\n\n```typescript\nimport Piscina from 'piscina';\nimport { join } from 'node:path';\n\nconst piscina = new Piscina({\n filename: join(import.meta.dirname, 'workers', 'compute.js'),\n});\n\napp.post('/compute', async (request) => {\n const result = await piscina.run(request.body);\n return result;\n});\n```\n\n```typescript\n// workers/compute.js\nexport default function compute(data) {\n // CPU-intensive work here\n return processedResult;\n}\n```\n\n## Stream Large Responses\n\nStream large payloads instead of buffering:\n\n```typescript\nimport { createReadStream } from 'node:fs';\nimport { pipeline } from 'node:stream/promises';\n\n// GOOD - stream file\napp.get('/large-file', async (request, reply) => {\n const stream = createReadStream('./large-file.json');\n reply.type('application/json');\n return reply.send(stream);\n});\n\n// BAD - load entire file into memory\napp.get('/large-file-bad', async () => {\n const content = await fs.readFile('./large-file.json', 'utf-8');\n return JSON.parse(content);\n});\n\n// Stream database results\napp.get('/export', async (request, reply) => {\n reply.type('application/json');\n\n const cursor = db.users.findCursor();\n reply.raw.write('[');\n\n let first = true;\n for await (const user of cursor) {\n if (!first) reply.raw.write(',');\n reply.raw.write(JSON.stringify(user));\n first = false;\n }\n\n reply.raw.write(']');\n reply.raw.end();\n});\n```\n\n## Caching Strategies\n\nImplement caching for expensive operations:\n\n```typescript\nimport { LRUCache } from 'lru-cache';\n\nconst cache = new LRUCache\u003cstring, unknown>({\n max: 1000,\n ttl: 60000, // 1 minute\n});\n\napp.get('/expensive/:id', async (request) => {\n const { id } = request.params;\n const cacheKey = `expensive:${id}`;\n\n const cached = cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n const result = await expensiveOperation(id);\n cache.set(cacheKey, result);\n\n return result;\n});\n\n// Cache control headers\napp.get('/static-data', async (request, reply) => {\n reply.header('Cache-Control', 'public, max-age=3600');\n return { data: 'static' };\n});\n```\n\n## Request Coalescing with async-cache-dedupe\n\nUse `async-cache-dedupe` for deduplicating concurrent identical requests and caching:\n\n```typescript\nimport { createCache } from 'async-cache-dedupe';\n\nconst cache = createCache({\n ttl: 60, // seconds\n stale: 5, // serve stale while revalidating\n storage: { type: 'memory' },\n});\n\ncache.define('fetchData', async (id: string) => {\n return db.findById(id);\n});\n\napp.get('/data/:id', async (request) => {\n const { id } = request.params;\n // Automatically deduplicates concurrent requests for the same id\n // and caches the result\n return cache.fetchData(id);\n});\n```\n\nFor distributed caching, use Redis storage:\n\n```typescript\nimport { createCache } from 'async-cache-dedupe';\nimport Redis from 'ioredis';\n\nconst redis = new Redis(process.env.REDIS_URL);\n\nconst cache = createCache({\n ttl: 60,\n storage: { type: 'redis', options: { client: redis } },\n});\n```\n\n## Payload Limits\n\nSet appropriate payload limits:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n bodyLimit: 1048576, // 1MB default\n});\n\n// Per-route limit for file uploads\napp.post('/upload', {\n bodyLimit: 10485760, // 10MB for this route\n}, uploadHandler);\n```\n\n## Compression\n\nUse compression for responses:\n\n```typescript\nimport fastifyCompress from '@fastify/compress';\n\napp.register(fastifyCompress, {\n global: true,\n threshold: 1024, // Only compress responses > 1KB\n encodings: ['gzip', 'deflate'],\n});\n\n// Disable for specific route\napp.get('/already-compressed', {\n compress: false,\n}, handler);\n```\n\n## Connection Timeouts\n\nConfigure appropriate timeouts:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n connectionTimeout: 30000, // 30 seconds\n keepAliveTimeout: 5000, // 5 seconds\n});\n\n// Per-route timeout\napp.get('/long-operation', {\n config: {\n timeout: 60000, // 60 seconds\n },\n}, async (request) => {\n return longOperation();\n});\n```\n\n## Disable Unnecessary Features\n\nDisable features you don't need:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n disableRequestLogging: true, // If you don't need request logs\n trustProxy: false, // If not behind proxy\n caseSensitive: true, // Enable for slight performance gain\n ignoreDuplicateSlashes: false,\n});\n```\n\n## Benchmarking\n\nUse autocannon for load testing:\n\n```bash\n# Install\nnpm install -g autocannon\n\n# Basic benchmark\nautocannon http://localhost:3000/api/users\n\n# With options\nautocannon -c 100 -d 30 -p 10 http://localhost:3000/api/users\n# -c: connections\n# -d: duration in seconds\n# -p: pipelining factor\n```\n\n```typescript\n// Programmatic benchmarking\nimport autocannon from 'autocannon';\n\nconst result = await autocannon({\n url: 'http://localhost:3000/api/users',\n connections: 100,\n duration: 30,\n pipelining: 10,\n});\n\nconsole.log(autocannon.printResult(result));\n```\n\n## Profiling\n\nUse `@platformatic/flame` for flame graph profiling:\n\n```bash\nnpx @platformatic/flame app.js\n```\n\nThis generates an interactive flame graph to identify performance bottlenecks.\n\n## Memory Management\n\nMonitor and optimize memory usage:\n\n```typescript\n// Add health endpoint with memory info\napp.get('/health', async () => {\n const memory = process.memoryUsage();\n return {\n status: 'ok',\n memory: {\n heapUsed: Math.round(memory.heapUsed / 1024 / 1024) + 'MB',\n heapTotal: Math.round(memory.heapTotal / 1024 / 1024) + 'MB',\n rss: Math.round(memory.rss / 1024 / 1024) + 'MB',\n },\n };\n});\n\n// Avoid memory leaks in closures\napp.addHook('onRequest', async (request) => {\n // BAD - holding reference to large object\n const largeData = await loadLargeData();\n request.getData = () => largeData;\n\n // GOOD - load on demand\n request.getData = () => loadLargeData();\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10130,"content_sha256":"c289c912f7b39d0c65cb7c3545dcda40767a948ce48d8d0eeccec21020410396"},{"filename":"rules/plugins.md","content":"---\nname: plugins\ndescription: Plugin development and encapsulation in Fastify\nmetadata:\n tags: plugins, encapsulation, modules, architecture\n---\n\n# Plugin Development and Encapsulation\n\n## Contents\n\n- [Understanding Encapsulation](#understanding-encapsulation)\n- [Breaking Encapsulation with fastify-plugin](#breaking-encapsulation-with-fastify-plugin)\n- [Plugin Registration Order](#plugin-registration-order)\n- [Plugin Options](#plugin-options)\n- [Plugin Factory Pattern](#plugin-factory-pattern)\n- [Plugin Dependencies](#plugin-dependencies)\n- [Scoped Plugins for Route Groups](#scoped-plugins-for-route-groups)\n- [Prefix Routes with Register](#prefix-routes-with-register)\n- [Plugin Metadata](#plugin-metadata)\n- [Autoload Plugins](#autoload-plugins)\n- [Testing Plugins in Isolation](#testing-plugins-in-isolation)\n\n## Understanding Encapsulation\n\nFastify's plugin system provides automatic encapsulation. Each plugin creates its own context, isolating decorators, hooks, and plugins registered within it:\n\n```typescript\nimport Fastify from 'fastify';\nimport fp from 'fastify-plugin';\n\nconst app = Fastify();\n\n// This plugin is encapsulated - its decorators are NOT available to siblings\napp.register(async function childPlugin(fastify) {\n fastify.decorate('privateUtil', () => 'only available here');\n\n // This decorator is only available within this plugin and its children\n fastify.get('/child', async function (request, reply) {\n return this.privateUtil();\n });\n});\n\n// This route CANNOT access privateUtil - it's in a different context\napp.get('/parent', async function (request, reply) {\n // this.privateUtil is undefined here\n return { status: 'ok' };\n});\n```\n\n## Breaking Encapsulation with fastify-plugin\n\nUse `fastify-plugin` when you need to share decorators, hooks, or plugins with the parent context:\n\n```typescript\nimport fp from 'fastify-plugin';\n\n// This plugin's decorators will be available to the parent and siblings\nexport default fp(async function databasePlugin(fastify, options) {\n const db = await createConnection(options.connectionString);\n\n fastify.decorate('db', db);\n\n fastify.addHook('onClose', async () => {\n await db.close();\n });\n}, {\n name: 'database-plugin',\n dependencies: [], // List plugin dependencies\n});\n```\n\n## Plugin Registration Order\n\nPlugins are registered in order, but loading is asynchronous. Use `after()` for sequential dependencies:\n\n```typescript\nimport Fastify from 'fastify';\nimport databasePlugin from './plugins/database.js';\nimport authPlugin from './plugins/auth.js';\nimport routesPlugin from './routes/index.js';\n\nconst app = Fastify();\n\n// Database must be ready before auth\napp.register(databasePlugin);\n\n// Auth depends on database\napp.register(authPlugin);\n\n// Routes depend on both\napp.register(routesPlugin);\n\n// Or use after() for explicit sequencing\napp.register(databasePlugin).after(() => {\n app.register(authPlugin).after(() => {\n app.register(routesPlugin);\n });\n});\n\nawait app.ready();\n```\n\n## Plugin Options\n\nAlways validate and document plugin options:\n\n```typescript\nimport fp from 'fastify-plugin';\n\ninterface CachePluginOptions {\n ttl: number;\n maxSize?: number;\n prefix?: string;\n}\n\nexport default fp\u003cCachePluginOptions>(async function cachePlugin(fastify, options) {\n const { ttl, maxSize = 1000, prefix = 'cache:' } = options;\n\n if (typeof ttl !== 'number' || ttl \u003c= 0) {\n throw new Error('Cache plugin requires a positive ttl option');\n }\n\n const cache = new Map\u003cstring, { value: unknown; expires: number }>();\n\n fastify.decorate('cache', {\n get(key: string): unknown | undefined {\n const item = cache.get(prefix + key);\n if (!item) return undefined;\n if (Date.now() > item.expires) {\n cache.delete(prefix + key);\n return undefined;\n }\n return item.value;\n },\n set(key: string, value: unknown): void {\n if (cache.size >= maxSize) {\n const firstKey = cache.keys().next().value;\n cache.delete(firstKey);\n }\n cache.set(prefix + key, { value, expires: Date.now() + ttl });\n },\n });\n}, {\n name: 'cache-plugin',\n});\n```\n\n## Plugin Factory Pattern\n\nCreate configurable plugins using factory functions:\n\n```typescript\nimport fp from 'fastify-plugin';\n\ninterface RateLimitOptions {\n max: number;\n timeWindow: number;\n}\n\nfunction createRateLimiter(defaults: Partial\u003cRateLimitOptions> = {}) {\n return fp\u003cRateLimitOptions>(async function rateLimitPlugin(fastify, options) {\n const config = { ...defaults, ...options };\n\n // Implementation\n fastify.decorate('rateLimit', config);\n }, {\n name: 'rate-limiter',\n });\n}\n\n// Usage\napp.register(createRateLimiter({ max: 100 }), { timeWindow: 60000 });\n```\n\n## Plugin Dependencies\n\nDeclare dependencies to ensure proper load order:\n\n```typescript\nimport fp from 'fastify-plugin';\n\nexport default fp(async function authPlugin(fastify) {\n // This plugin requires 'database-plugin' to be loaded first\n if (!fastify.hasDecorator('db')) {\n throw new Error('Auth plugin requires database plugin');\n }\n\n fastify.decorate('authenticate', async (request) => {\n const user = await fastify.db.users.findByToken(request.headers.authorization);\n return user;\n });\n}, {\n name: 'auth-plugin',\n dependencies: ['database-plugin'],\n});\n```\n\n## Scoped Plugins for Route Groups\n\nUse encapsulation to scope plugins to specific routes:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify();\n\n// Public routes - no auth required\napp.register(async function publicRoutes(fastify) {\n fastify.get('/health', async () => ({ status: 'ok' }));\n fastify.get('/docs', async () => ({ version: '1.0.0' }));\n});\n\n// Protected routes - auth required\napp.register(async function protectedRoutes(fastify) {\n // Auth hook only applies to routes in this plugin\n fastify.addHook('onRequest', async (request, reply) => {\n const token = request.headers.authorization;\n if (!token) {\n reply.code(401).send({ error: 'Unauthorized' });\n return;\n }\n request.user = await verifyToken(token);\n });\n\n fastify.get('/profile', async (request) => {\n return { user: request.user };\n });\n\n fastify.get('/settings', async (request) => {\n return { settings: await getSettings(request.user.id) };\n });\n});\n```\n\n## Prefix Routes with Register\n\nUse the `prefix` option to namespace routes:\n\n```typescript\napp.register(import('./routes/users.js'), { prefix: '/api/v1/users' });\napp.register(import('./routes/posts.js'), { prefix: '/api/v1/posts' });\n\n// In routes/users.js\nexport default async function userRoutes(fastify) {\n // Becomes /api/v1/users\n fastify.get('/', async () => {\n return { users: [] };\n });\n\n // Becomes /api/v1/users/:id\n fastify.get('/:id', async (request) => {\n return { user: { id: request.params.id } };\n });\n}\n```\n\n## Plugin Metadata\n\nAdd metadata for documentation and tooling:\n\n```typescript\nimport fp from 'fastify-plugin';\n\nasync function metricsPlugin(fastify) {\n // Implementation\n}\n\nexport default fp(metricsPlugin, {\n name: 'metrics-plugin',\n fastify: '5.x', // Fastify version compatibility\n dependencies: ['pino-plugin'],\n decorators: {\n fastify: ['db'], // Required decorators\n request: [],\n reply: [],\n },\n});\n```\n\n## Autoload Plugins\n\nUse `@fastify/autoload` for automatic plugin loading:\n\n```typescript\nimport Fastify from 'fastify';\nimport autoload from '@fastify/autoload';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nconst app = Fastify();\n\n// Load all plugins from the plugins directory\napp.register(autoload, {\n dir: join(__dirname, 'plugins'),\n options: { prefix: '/api' },\n});\n\n// Load all routes from the routes directory\napp.register(autoload, {\n dir: join(__dirname, 'routes'),\n options: { prefix: '/api' },\n});\n```\n\n## Testing Plugins in Isolation\n\nTest plugins independently:\n\n```typescript\nimport { describe, it, before, after } from 'node:test';\nimport Fastify from 'fastify';\nimport myPlugin from './my-plugin.js';\n\ndescribe('MyPlugin', () => {\n let app;\n\n before(async () => {\n app = Fastify();\n app.register(myPlugin, { option: 'value' });\n await app.ready();\n });\n\n after(async () => {\n await app.close();\n });\n\n it('should decorate fastify instance', (t) => {\n t.assert.ok(app.hasDecorator('myDecorator'));\n });\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8393,"content_sha256":"03c2c5b857321a6d826ee06010894c08d975278ea62aa4b6d9cfda82839e82fc"},{"filename":"rules/routes.md","content":"---\nname: routes\ndescription: Route organization and handlers in Fastify\nmetadata:\n tags: routes, handlers, http, rest, api\n---\n\n# Route Organization and Handlers\n\n## Contents\n\n- [Basic Route Definition](#basic-route-definition)\n- [Route Parameters](#route-parameters)\n- [Query String Parameters](#query-string-parameters)\n- [Request Body](#request-body)\n- [Headers](#headers)\n- [Reply Methods](#reply-methods)\n- [Route Organization by Feature](#route-organization-by-feature)\n- [Route Constraints](#route-constraints)\n- [Route Prefixing](#route-prefixing)\n- [Multiple Methods](#multiple-methods)\n- [404 Handler](#404-handler)\n- [Method Not Allowed](#method-not-allowed)\n- [Route-Level Configuration](#route-level-configuration)\n- [Async Route Registration](#async-route-registration)\n- [Auto-loading Routes with @fastify/autoload](#auto-loading-routes-with-fastifyautoload)\n\n## Basic Route Definition\n\nDefine routes with the shorthand methods or the full route method:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify();\n\n// Shorthand methods\napp.get('/users', async (request, reply) => {\n return { users: [] };\n});\n\napp.post('/users', async (request, reply) => {\n return { created: true };\n});\n\n// Full route method with all options\napp.route({\n method: 'GET',\n url: '/users/:id',\n schema: {\n params: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n },\n required: ['id'],\n },\n },\n handler: async (request, reply) => {\n return { id: request.params.id };\n },\n});\n```\n\n## Route Parameters\n\nAccess URL parameters through `request.params`:\n\n```typescript\n// Single parameter\napp.get('/users/:id', async (request) => {\n const { id } = request.params as { id: string };\n return { userId: id };\n});\n\n// Multiple parameters\napp.get('/users/:userId/posts/:postId', async (request) => {\n const { userId, postId } = request.params as { userId: string; postId: string };\n return { userId, postId };\n});\n\n// Wildcard parameter (captures everything after)\napp.get('/files/*', async (request) => {\n const path = (request.params as { '*': string })['*'];\n return { filePath: path };\n});\n\n// Regex parameters (Fastify uses find-my-way)\napp.get('/orders/:id(\\\\d+)', async (request) => {\n // Only matches numeric IDs\n const { id } = request.params as { id: string };\n return { orderId: parseInt(id, 10) };\n});\n```\n\n## Query String Parameters\n\nAccess query parameters through `request.query`:\n\n```typescript\napp.get('/search', {\n schema: {\n querystring: {\n type: 'object',\n properties: {\n q: { type: 'string' },\n page: { type: 'integer', default: 1 },\n limit: { type: 'integer', default: 10, maximum: 100 },\n },\n required: ['q'],\n },\n },\n handler: async (request) => {\n const { q, page, limit } = request.query as {\n q: string;\n page: number;\n limit: number;\n };\n return { query: q, page, limit };\n },\n});\n```\n\n## Request Body\n\nAccess the request body through `request.body`:\n\n```typescript\napp.post('/users', {\n schema: {\n body: {\n type: 'object',\n properties: {\n name: { type: 'string', minLength: 1 },\n email: { type: 'string', format: 'email' },\n age: { type: 'integer', minimum: 0 },\n },\n required: ['name', 'email'],\n },\n },\n handler: async (request, reply) => {\n const user = request.body as { name: string; email: string; age?: number };\n // Create user...\n reply.code(201);\n return { user };\n },\n});\n```\n\n## Headers\n\nAccess request headers through `request.headers`:\n\n```typescript\napp.get('/protected', {\n schema: {\n headers: {\n type: 'object',\n properties: {\n authorization: { type: 'string' },\n },\n required: ['authorization'],\n },\n },\n handler: async (request) => {\n const token = request.headers.authorization;\n return { authenticated: true };\n },\n});\n```\n\n## Reply Methods\n\nUse reply methods to control the response:\n\n```typescript\napp.get('/examples', async (request, reply) => {\n // Set status code\n reply.code(201);\n\n // Set headers\n reply.header('X-Custom-Header', 'value');\n reply.headers({ 'X-Another': 'value', 'X-Third': 'value' });\n\n // Set content type\n reply.type('application/json');\n\n // Redirect\n // reply.redirect('/other-url');\n // reply.redirect(301, '/permanent-redirect');\n\n // Return response (automatic serialization)\n return { status: 'ok' };\n});\n\n// Explicit send (useful in non-async handlers)\napp.get('/explicit', (request, reply) => {\n reply.send({ status: 'ok' });\n});\n\n// Stream response\napp.get('/stream', async (request, reply) => {\n const stream = fs.createReadStream('./large-file.txt');\n reply.type('text/plain');\n return reply.send(stream);\n});\n```\n\n## Route Organization by Feature\n\nOrganize routes by feature/domain in separate files:\n\n```\nsrc/\n routes/\n users/\n index.ts # Route definitions\n handlers.ts # Handler functions\n schemas.ts # JSON schemas\n posts/\n index.ts\n handlers.ts\n schemas.ts\n```\n\n```typescript\n// routes/users/schemas.ts\nexport const userSchema = {\n type: 'object',\n properties: {\n id: { type: 'string', format: 'uuid' },\n name: { type: 'string' },\n email: { type: 'string', format: 'email' },\n },\n};\n\nexport const createUserSchema = {\n body: {\n type: 'object',\n properties: {\n name: { type: 'string', minLength: 1 },\n email: { type: 'string', format: 'email' },\n },\n required: ['name', 'email'],\n },\n response: {\n 201: userSchema,\n },\n};\n\n// routes/users/handlers.ts\nimport type { FastifyRequest, FastifyReply } from 'fastify';\n\nexport async function createUser(\n request: FastifyRequest\u003c{ Body: { name: string; email: string } }>,\n reply: FastifyReply,\n) {\n const { name, email } = request.body;\n const user = await request.server.db.users.create({ name, email });\n reply.code(201);\n return user;\n}\n\nexport async function getUsers(request: FastifyRequest) {\n return request.server.db.users.findAll();\n}\n\n// routes/users/index.ts\nimport type { FastifyInstance } from 'fastify';\nimport { createUser, getUsers } from './handlers.js';\nimport { createUserSchema } from './schemas.js';\n\nexport default async function userRoutes(fastify: FastifyInstance) {\n fastify.get('/', getUsers);\n fastify.post('/', { schema: createUserSchema }, createUser);\n}\n```\n\n## Route Constraints\n\nAdd constraints to routes for versioning or host-based routing:\n\n```typescript\n// Version constraint\napp.get('/users', {\n constraints: { version: '1.0.0' },\n handler: async () => ({ version: '1.0.0', users: [] }),\n});\n\napp.get('/users', {\n constraints: { version: '2.0.0' },\n handler: async () => ({ version: '2.0.0', data: { users: [] } }),\n});\n\n// Client sends: Accept-Version: 1.0.0\n\n// Host constraint\napp.get('/', {\n constraints: { host: 'api.example.com' },\n handler: async () => ({ api: true }),\n});\n\napp.get('/', {\n constraints: { host: 'www.example.com' },\n handler: async () => ({ web: true }),\n});\n```\n\n## Route Prefixing\n\nUse prefixes to namespace routes:\n\n```typescript\n// Using register\napp.register(async function (fastify) {\n fastify.get('/list', async () => ({ users: [] }));\n fastify.get('/:id', async (request) => ({ id: request.params.id }));\n}, { prefix: '/users' });\n\n// Results in:\n// GET /users/list\n// GET /users/:id\n```\n\n## Multiple Methods\n\nHandle multiple HTTP methods with one handler:\n\n```typescript\napp.route({\n method: ['GET', 'HEAD'],\n url: '/resource',\n handler: async (request) => {\n return { data: 'resource' };\n },\n});\n```\n\n## 404 Handler\n\nCustomize the not found handler:\n\n```typescript\napp.setNotFoundHandler({\n preValidation: async (request, reply) => {\n // Optional pre-validation hook\n },\n preHandler: async (request, reply) => {\n // Optional pre-handler hook\n },\n}, async (request, reply) => {\n reply.code(404);\n return {\n error: 'Not Found',\n message: `Route ${request.method} ${request.url} not found`,\n statusCode: 404,\n };\n});\n```\n\n## Method Not Allowed\n\nHandle method not allowed responses:\n\n```typescript\n// Fastify doesn't have built-in 405 handling\n// Implement with a custom not found handler that checks allowed methods\napp.setNotFoundHandler(async (request, reply) => {\n // Check if the URL exists with a different method\n const route = app.hasRoute({\n url: request.url,\n method: 'GET', // Check other methods\n });\n\n if (route) {\n reply.code(405);\n return { error: 'Method Not Allowed' };\n }\n\n reply.code(404);\n return { error: 'Not Found' };\n});\n```\n\n## Route-Level Configuration\n\nApply configuration to specific routes:\n\n```typescript\napp.get('/slow-operation', {\n config: {\n rateLimit: { max: 10, timeWindow: '1 minute' },\n },\n handler: async (request) => {\n return { result: await slowOperation() };\n },\n});\n\n// Access config in hooks\napp.addHook('onRequest', async (request, reply) => {\n const config = request.routeOptions.config;\n if (config.rateLimit) {\n // Apply rate limiting\n }\n});\n```\n\n## Async Route Registration\n\nRegister routes from async sources:\n\n```typescript\napp.register(async function (fastify) {\n const routeConfigs = await loadRoutesFromDatabase();\n\n for (const config of routeConfigs) {\n fastify.route({\n method: config.method,\n url: config.path,\n handler: createDynamicHandler(config),\n });\n }\n});\n```\n\n## Auto-loading Routes with @fastify/autoload\n\nUse `@fastify/autoload` to automatically load routes from a directory structure:\n\n```typescript\nimport Fastify from 'fastify';\nimport autoload from '@fastify/autoload';\nimport { join } from 'node:path';\n\nconst app = Fastify({ logger: true });\n\n// Auto-load plugins\napp.register(autoload, {\n dir: join(import.meta.dirname, 'plugins'),\n options: { prefix: '' },\n});\n\n// Auto-load routes\napp.register(autoload, {\n dir: join(import.meta.dirname, 'routes'),\n options: { prefix: '/api' },\n});\n\nawait app.listen({ port: 3000 });\n```\n\nDirectory structure:\n\n```\nsrc/\n plugins/\n database.ts # Loaded automatically\n auth.ts # Loaded automatically\n routes/\n users/\n index.ts # GET/POST /api/users\n _id/\n index.ts # GET/PUT/DELETE /api/users/:id\n posts/\n index.ts # GET/POST /api/posts\n```\n\nRoute file example:\n\n```typescript\n// routes/users/index.ts\nimport type { FastifyPluginAsync } from 'fastify';\n\nconst users: FastifyPluginAsync = async (fastify) => {\n fastify.get('/', async () => {\n return fastify.repositories.users.findAll();\n });\n\n fastify.post('/', async (request) => {\n return fastify.repositories.users.create(request.body);\n });\n};\n\nexport default users;\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10667,"content_sha256":"c8f3a17b4f85e2080222cf9822015e41b02455c55b51cdb337de2137fac1fefb"},{"filename":"rules/schemas.md","content":"---\nname: schemas\ndescription: JSON Schema validation in Fastify with TypeBox\nmetadata:\n tags: validation, json-schema, schemas, ajv, typebox\n---\n\n# JSON Schema Validation\n\n## Contents\n\n- [Use TypeBox for Type-Safe Schemas](#use-typebox-for-type-safe-schemas)\n- [TypeBox Common Patterns](#typebox-common-patterns)\n- [Register TypeBox Schemas Globally](#register-typebox-schemas-globally)\n- [Plain JSON Schema (Alternative)](#plain-json-schema-alternative)\n- [Request Validation Parts](#request-validation-parts)\n- [Shared Schemas with $id](#shared-schemas-with-id)\n- [Array Schemas](#array-schemas)\n- [Custom Formats](#custom-formats)\n- [Custom Keywords](#custom-keywords)\n- [Coercion](#coercion)\n- [Validation Error Handling](#validation-error-handling)\n- [Schema Compiler Options](#schema-compiler-options)\n- [Nullable Fields](#nullable-fields)\n- [Conditional Validation](#conditional-validation)\n- [Schema Organization](#schema-organization)\n- [OpenAPI/Swagger Integration](#openapiswagger-integration)\n- [Performance Considerations](#performance-considerations)\n\n## Use TypeBox for Type-Safe Schemas\n\n**Prefer TypeBox for defining schemas.** It provides TypeScript types automatically and compiles to JSON Schema:\n\n```typescript\nimport Fastify from 'fastify';\nimport { Type, type Static } from '@sinclair/typebox';\n\nconst app = Fastify();\n\n// Define schema with TypeBox - get TypeScript types for free\nconst CreateUserBody = Type.Object({\n name: Type.String({ minLength: 1, maxLength: 100 }),\n email: Type.String({ format: 'email' }),\n age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),\n});\n\nconst UserResponse = Type.Object({\n id: Type.String({ format: 'uuid' }),\n name: Type.String(),\n email: Type.String(),\n createdAt: Type.String({ format: 'date-time' }),\n});\n\n// TypeScript types are derived automatically\ntype CreateUserBodyType = Static\u003ctypeof CreateUserBody>;\ntype UserResponseType = Static\u003ctypeof UserResponse>;\n\napp.post\u003c{\n Body: CreateUserBodyType;\n Reply: UserResponseType;\n}>('/users', {\n schema: {\n body: CreateUserBody,\n response: {\n 201: UserResponse,\n },\n },\n}, async (request, reply) => {\n // request.body is fully typed as CreateUserBodyType\n const user = await createUser(request.body);\n reply.code(201);\n return user;\n});\n```\n\n## TypeBox Common Patterns\n\n```typescript\nimport { Type, type Static } from '@sinclair/typebox';\n\n// Enums\nconst Status = Type.Union([\n Type.Literal('active'),\n Type.Literal('inactive'),\n Type.Literal('pending'),\n]);\n\n// Arrays\nconst Tags = Type.Array(Type.String(), { minItems: 1, maxItems: 10 });\n\n// Nested objects\nconst Address = Type.Object({\n street: Type.String(),\n city: Type.String(),\n country: Type.String(),\n zip: Type.Optional(Type.String()),\n});\n\n// References (reusable schemas)\nconst User = Type.Object({\n id: Type.String({ format: 'uuid' }),\n name: Type.String(),\n address: Address,\n tags: Tags,\n status: Status,\n});\n\n// Nullable\nconst NullableString = Type.Union([Type.String(), Type.Null()]);\n\n// Record/Map\nconst Metadata = Type.Record(Type.String(), Type.Unknown());\n```\n\n## Register TypeBox Schemas Globally\n\n```typescript\nimport { Type, type Static } from '@sinclair/typebox';\n\n// Define shared schemas\nconst ErrorResponse = Type.Object({\n error: Type.String(),\n message: Type.String(),\n statusCode: Type.Integer(),\n});\n\nconst PaginationQuery = Type.Object({\n page: Type.Integer({ minimum: 1, default: 1 }),\n limit: Type.Integer({ minimum: 1, maximum: 100, default: 20 }),\n});\n\n// Register globally\napp.addSchema(Type.Object({ $id: 'ErrorResponse', ...ErrorResponse }));\napp.addSchema(Type.Object({ $id: 'PaginationQuery', ...PaginationQuery }));\n\n// Reference in routes\napp.get('/items', {\n schema: {\n querystring: { $ref: 'PaginationQuery#' },\n response: {\n 400: { $ref: 'ErrorResponse#' },\n },\n },\n}, handler);\n```\n\n## Plain JSON Schema (Alternative)\n\nYou can also use plain JSON Schema directly:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify();\n\nconst createUserSchema = {\n body: {\n type: 'object',\n properties: {\n name: { type: 'string', minLength: 1, maxLength: 100 },\n email: { type: 'string', format: 'email' },\n age: { type: 'integer', minimum: 0, maximum: 150 },\n },\n required: ['name', 'email'],\n additionalProperties: false,\n },\n response: {\n 201: {\n type: 'object',\n properties: {\n id: { type: 'string', format: 'uuid' },\n name: { type: 'string' },\n email: { type: 'string' },\n createdAt: { type: 'string', format: 'date-time' },\n },\n },\n },\n};\n\napp.post('/users', { schema: createUserSchema }, async (request, reply) => {\n const user = await createUser(request.body);\n reply.code(201);\n return user;\n});\n```\n\n## Request Validation Parts\n\nValidate different parts of the request:\n\n```typescript\nconst fullRequestSchema = {\n // URL parameters\n params: {\n type: 'object',\n properties: {\n id: { type: 'string', format: 'uuid' },\n },\n required: ['id'],\n },\n\n // Query string\n querystring: {\n type: 'object',\n properties: {\n include: { type: 'string', enum: ['posts', 'comments', 'all'] },\n limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },\n },\n },\n\n // Request headers\n headers: {\n type: 'object',\n properties: {\n 'x-api-key': { type: 'string', minLength: 32 },\n },\n required: ['x-api-key'],\n },\n\n // Request body\n body: {\n type: 'object',\n properties: {\n data: { type: 'object' },\n },\n required: ['data'],\n },\n};\n\napp.put('/resources/:id', { schema: fullRequestSchema }, handler);\n```\n\n## Shared Schemas with $id\n\nDefine reusable schemas with `$id` and reference them with `$ref`:\n\n```typescript\n// Add shared schemas to Fastify\napp.addSchema({\n $id: 'user',\n type: 'object',\n properties: {\n id: { type: 'string', format: 'uuid' },\n name: { type: 'string' },\n email: { type: 'string', format: 'email' },\n createdAt: { type: 'string', format: 'date-time' },\n },\n required: ['id', 'name', 'email'],\n});\n\napp.addSchema({\n $id: 'userCreate',\n type: 'object',\n properties: {\n name: { type: 'string', minLength: 1 },\n email: { type: 'string', format: 'email' },\n },\n required: ['name', 'email'],\n additionalProperties: false,\n});\n\napp.addSchema({\n $id: 'error',\n type: 'object',\n properties: {\n statusCode: { type: 'integer' },\n error: { type: 'string' },\n message: { type: 'string' },\n },\n});\n\n// Reference shared schemas\napp.post('/users', {\n schema: {\n body: { $ref: 'userCreate#' },\n response: {\n 201: { $ref: 'user#' },\n 400: { $ref: 'error#' },\n },\n },\n}, handler);\n\napp.get('/users/:id', {\n schema: {\n params: {\n type: 'object',\n properties: { id: { type: 'string', format: 'uuid' } },\n required: ['id'],\n },\n response: {\n 200: { $ref: 'user#' },\n 404: { $ref: 'error#' },\n },\n },\n}, handler);\n```\n\n## Array Schemas\n\nDefine schemas for array responses:\n\n```typescript\napp.addSchema({\n $id: 'userList',\n type: 'object',\n properties: {\n users: {\n type: 'array',\n items: { $ref: 'user#' },\n },\n total: { type: 'integer' },\n page: { type: 'integer' },\n pageSize: { type: 'integer' },\n },\n});\n\napp.get('/users', {\n schema: {\n querystring: {\n type: 'object',\n properties: {\n page: { type: 'integer', minimum: 1, default: 1 },\n pageSize: { type: 'integer', minimum: 1, maximum: 100, default: 20 },\n },\n },\n response: {\n 200: { $ref: 'userList#' },\n },\n },\n}, handler);\n```\n\n## Custom Formats\n\nAdd custom validation formats:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n ajv: {\n customOptions: {\n formats: {\n 'iso-country': /^[A-Z]{2}$/,\n 'phone': /^\\+?[1-9]\\d{1,14}$/,\n },\n },\n },\n});\n\n// Or add formats dynamically\napp.addSchema({\n $id: 'address',\n type: 'object',\n properties: {\n street: { type: 'string' },\n country: { type: 'string', format: 'iso-country' },\n phone: { type: 'string', format: 'phone' },\n },\n});\n```\n\n## Custom Keywords\n\nAdd custom validation keywords:\n\n```typescript\nimport Fastify from 'fastify';\nimport Ajv from 'ajv';\n\nconst app = Fastify({\n ajv: {\n customOptions: {\n keywords: [\n {\n keyword: 'isEven',\n type: 'number',\n validate: (schema: boolean, data: number) => {\n if (schema) {\n return data % 2 === 0;\n }\n return true;\n },\n errors: false,\n },\n ],\n },\n },\n});\n\n// Use custom keyword\napp.post('/numbers', {\n schema: {\n body: {\n type: 'object',\n properties: {\n value: { type: 'integer', isEven: true },\n },\n },\n },\n}, handler);\n```\n\n## Coercion\n\nFastify coerces types by default for query strings and params:\n\n```typescript\n// Query string \"?page=5&active=true\" becomes:\n// { page: 5, active: true } (number and boolean, not strings)\n\napp.get('/items', {\n schema: {\n querystring: {\n type: 'object',\n properties: {\n page: { type: 'integer' }, // \"5\" -> 5\n active: { type: 'boolean' }, // \"true\" -> true\n tags: {\n type: 'array',\n items: { type: 'string' }, // \"a,b,c\" -> [\"a\", \"b\", \"c\"]\n },\n },\n },\n },\n}, handler);\n```\n\n## Validation Error Handling\n\nCustomize validation error responses:\n\n```typescript\napp.setErrorHandler((error, request, reply) => {\n if (error.validation) {\n reply.code(400).send({\n error: 'Validation Error',\n message: 'Request validation failed',\n details: error.validation.map((err) => ({\n field: err.instancePath || err.params?.missingProperty,\n message: err.message,\n keyword: err.keyword,\n })),\n });\n return;\n }\n\n // Handle other errors\n reply.code(error.statusCode || 500).send({\n error: error.name,\n message: error.message,\n });\n});\n```\n\n## Schema Compiler Options\n\nConfigure the Ajv schema compiler:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n ajv: {\n customOptions: {\n removeAdditional: 'all', // Remove extra properties\n useDefaults: true, // Apply default values\n coerceTypes: true, // Coerce types\n allErrors: true, // Report all errors, not just first\n },\n plugins: [\n require('ajv-formats'), // Add format validators\n ],\n },\n});\n```\n\n## Nullable Fields\n\nHandle nullable fields properly:\n\n```typescript\napp.addSchema({\n $id: 'profile',\n type: 'object',\n properties: {\n name: { type: 'string' },\n bio: { type: ['string', 'null'] }, // Can be string or null\n avatar: {\n oneOf: [\n { type: 'string', format: 'uri' },\n { type: 'null' },\n ],\n },\n },\n});\n```\n\n## Conditional Validation\n\nUse if/then/else for conditional validation:\n\n```typescript\napp.addSchema({\n $id: 'payment',\n type: 'object',\n properties: {\n method: { type: 'string', enum: ['card', 'bank'] },\n cardNumber: { type: 'string' },\n bankAccount: { type: 'string' },\n },\n required: ['method'],\n if: {\n properties: { method: { const: 'card' } },\n },\n then: {\n required: ['cardNumber'],\n },\n else: {\n required: ['bankAccount'],\n },\n});\n```\n\n## Schema Organization\n\nOrganize schemas in a dedicated file:\n\n```typescript\n// schemas/index.ts\nexport const schemas = [\n {\n $id: 'user',\n type: 'object',\n properties: {\n id: { type: 'string', format: 'uuid' },\n name: { type: 'string' },\n email: { type: 'string', format: 'email' },\n },\n },\n {\n $id: 'error',\n type: 'object',\n properties: {\n statusCode: { type: 'integer' },\n error: { type: 'string' },\n message: { type: 'string' },\n },\n },\n];\n\n// app.ts\nimport { schemas } from './schemas/index.js';\n\nfor (const schema of schemas) {\n app.addSchema(schema);\n}\n```\n\n## OpenAPI/Swagger Integration\n\nSchemas work directly with @fastify/swagger:\n\n```typescript\nimport fastifySwagger from '@fastify/swagger';\nimport fastifySwaggerUi from '@fastify/swagger-ui';\n\napp.register(fastifySwagger, {\n openapi: {\n info: {\n title: 'My API',\n version: '1.0.0',\n },\n },\n});\n\napp.register(fastifySwaggerUi, {\n routePrefix: '/docs',\n});\n\n// Schemas are automatically converted to OpenAPI definitions\n```\n\n## Performance Considerations\n\nResponse schemas enable fast-json-stringify for serialization:\n\n```typescript\n// With response schema - uses fast-json-stringify (faster)\napp.get('/users', {\n schema: {\n response: {\n 200: {\n type: 'array',\n items: { $ref: 'user#' },\n },\n },\n },\n}, handler);\n\n// Without response schema - uses JSON.stringify (slower)\napp.get('/users-slow', handler);\n```\n\nAlways define response schemas for production APIs to benefit from optimized serialization.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12899,"content_sha256":"950b5bed259b77fc675123815c4b5d1afa0ec896c5dd4a32089486f815ac2e6f"},{"filename":"rules/serialization.md","content":"---\nname: serialization\ndescription: Response serialization in Fastify with TypeBox\nmetadata:\n tags: serialization, response, json, fast-json-stringify, typebox\n---\n\n# Response Serialization\n\n## Contents\n\n- [Use TypeBox for Type-Safe Response Schemas](#use-typebox-for-type-safe-response-schemas)\n- [Fast JSON Stringify](#fast-json-stringify)\n- [Response Schema Benefits](#response-schema-benefits)\n- [Multiple Status Code Schemas](#multiple-status-code-schemas)\n- [Default Response Schema](#default-response-schema)\n- [Custom Serializers](#custom-serializers)\n- [Shared Serializers](#shared-serializers)\n- [Serialization with Type Coercion](#serialization-with-type-coercion)\n- [Nullable Fields](#nullable-fields)\n- [Additional Properties](#additional-properties)\n- [Nested Objects](#nested-objects)\n- [Date Serialization](#date-serialization)\n- [BigInt Serialization](#bigint-serialization)\n- [Stream Responses](#stream-responses)\n- [Pre-Serialization Hook](#pre-serialization-hook)\n- [Disable Serialization](#disable-serialization)\n\n## Use TypeBox for Type-Safe Response Schemas\n\nDefine response schemas with TypeBox for automatic TypeScript types and fast serialization:\n\n```typescript\nimport Fastify from 'fastify';\nimport { Type, type Static } from '@sinclair/typebox';\n\nconst app = Fastify();\n\n// Define response schema with TypeBox\nconst UserResponse = Type.Object({\n id: Type.String(),\n name: Type.String(),\n email: Type.String(),\n});\n\nconst UsersResponse = Type.Array(UserResponse);\n\ntype UserResponseType = Static\u003ctypeof UserResponse>;\n\n// With TypeBox schema - uses fast-json-stringify (faster) + TypeScript types\napp.get\u003c{ Reply: Static\u003ctypeof UsersResponse> }>('/users', {\n schema: {\n response: {\n 200: UsersResponse,\n },\n },\n}, async () => {\n return db.users.findAll();\n});\n\n// Without schema - uses JSON.stringify (slower), no type safety\napp.get('/users-slow', async () => {\n return db.users.findAll();\n});\n```\n\n## Fast JSON Stringify\n\nFastify uses `fast-json-stringify` when response schemas are defined. This provides:\n\n1. **Performance**: 2-3x faster serialization than JSON.stringify\n2. **Security**: Only defined properties are serialized (strips sensitive data)\n3. **Type coercion**: Ensures output matches the schema\n4. **TypeScript**: Full type inference with TypeBox\n\n## Response Schema Benefits\n\n1. **Performance**: 2-3x faster serialization\n2. **Security**: Only defined properties are included\n3. **Documentation**: OpenAPI/Swagger integration\n4. **Type coercion**: Ensures correct output types\n\n```typescript\napp.get('/user/:id', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n // password is NOT in schema, so it's stripped\n },\n },\n },\n },\n}, async (request) => {\n const user = await db.users.findById(request.params.id);\n // Even if user has password field, it won't be serialized\n return user;\n});\n```\n\n## Multiple Status Code Schemas\n\nDefine schemas for different response codes:\n\n```typescript\napp.get('/users/:id', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n email: { type: 'string' },\n },\n },\n 404: {\n type: 'object',\n properties: {\n statusCode: { type: 'integer' },\n error: { type: 'string' },\n message: { type: 'string' },\n },\n },\n },\n },\n}, async (request, reply) => {\n const user = await db.users.findById(request.params.id);\n\n if (!user) {\n reply.code(404);\n return { statusCode: 404, error: 'Not Found', message: 'User not found' };\n }\n\n return user;\n});\n```\n\n## Default Response Schema\n\nUse 'default' for common error responses:\n\n```typescript\napp.get('/resource', {\n schema: {\n response: {\n 200: { $ref: 'resource#' },\n '4xx': {\n type: 'object',\n properties: {\n statusCode: { type: 'integer' },\n error: { type: 'string' },\n message: { type: 'string' },\n },\n },\n '5xx': {\n type: 'object',\n properties: {\n statusCode: { type: 'integer' },\n error: { type: 'string' },\n },\n },\n },\n },\n}, handler);\n```\n\n## Custom Serializers\n\nCreate custom serialization functions:\n\n```typescript\n// Per-route serializer\napp.get('/custom', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n value: { type: 'string' },\n },\n },\n },\n },\n serializerCompiler: ({ schema }) => {\n return (data) => {\n // Custom serialization logic\n return JSON.stringify({\n value: String(data.value).toUpperCase(),\n serializedAt: new Date().toISOString(),\n });\n };\n },\n}, async () => {\n return { value: 'hello' };\n});\n```\n\n## Shared Serializers\n\nUse the global serializer compiler:\n\n```typescript\nimport Fastify from 'fastify';\n\nconst app = Fastify({\n serializerCompiler: ({ schema, method, url, httpStatus }) => {\n // Custom compilation logic\n const stringify = fastJson(schema);\n return (data) => stringify(data);\n },\n});\n```\n\n## Serialization with Type Coercion\n\nfast-json-stringify coerces types:\n\n```typescript\napp.get('/data', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n count: { type: 'integer' }, // '5' -> 5\n active: { type: 'boolean' }, // 'true' -> true\n tags: {\n type: 'array',\n items: { type: 'string' }, // [1, 2] -> ['1', '2']\n },\n },\n },\n },\n },\n}, async () => {\n return {\n count: '5', // Coerced to integer\n active: 'true', // Coerced to boolean\n tags: [1, 2, 3], // Coerced to strings\n };\n});\n```\n\n## Nullable Fields\n\nHandle nullable fields properly:\n\n```typescript\napp.get('/profile', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n name: { type: 'string' },\n bio: { type: ['string', 'null'] },\n avatar: {\n oneOf: [\n { type: 'string', format: 'uri' },\n { type: 'null' },\n ],\n },\n },\n },\n },\n },\n}, async () => {\n return {\n name: 'John',\n bio: null,\n avatar: null,\n };\n});\n```\n\n## Additional Properties\n\nControl extra properties in response:\n\n```typescript\n// Strip additional properties (default)\napp.get('/strict', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n },\n additionalProperties: false,\n },\n },\n },\n}, async () => {\n return { id: '1', name: 'John', secret: 'hidden' };\n // Output: { \"id\": \"1\", \"name\": \"John\" }\n});\n\n// Allow additional properties\napp.get('/flexible', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n },\n additionalProperties: true,\n },\n },\n },\n}, async () => {\n return { id: '1', extra: 'included' };\n // Output: { \"id\": \"1\", \"extra\": \"included\" }\n});\n```\n\n## Nested Objects\n\nSerialize nested structures:\n\n```typescript\napp.addSchema({\n $id: 'address',\n type: 'object',\n properties: {\n street: { type: 'string' },\n city: { type: 'string' },\n country: { type: 'string' },\n },\n});\n\napp.get('/user', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n name: { type: 'string' },\n address: { $ref: 'address#' },\n contacts: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n type: { type: 'string' },\n value: { type: 'string' },\n },\n },\n },\n },\n },\n },\n },\n}, async () => {\n return {\n name: 'John',\n address: { street: '123 Main', city: 'Boston', country: 'USA' },\n contacts: [\n { type: 'email', value: '[email protected]' },\n { type: 'phone', value: '+1234567890' },\n ],\n };\n});\n```\n\n## Date Serialization\n\nHandle dates consistently:\n\n```typescript\napp.get('/events', {\n schema: {\n response: {\n 200: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n name: { type: 'string' },\n date: { type: 'string', format: 'date-time' },\n },\n },\n },\n },\n },\n}, async () => {\n const events = await db.events.findAll();\n\n // Convert Date objects to ISO strings\n return events.map((e) => ({\n ...e,\n date: e.date.toISOString(),\n }));\n});\n```\n\n## BigInt Serialization\n\nHandle BigInt values:\n\n```typescript\n// BigInt is not JSON serializable by default\napp.get('/large-number', {\n schema: {\n response: {\n 200: {\n type: 'object',\n properties: {\n id: { type: 'string' }, // Serialize as string\n count: { type: 'integer' },\n },\n },\n },\n },\n}, async () => {\n const bigValue = 9007199254740993n;\n\n return {\n id: bigValue.toString(), // Convert to string\n count: Number(bigValue), // Or number if safe\n };\n});\n```\n\n## Stream Responses\n\nStream responses bypass serialization:\n\n```typescript\nimport { createReadStream } from 'node:fs';\n\napp.get('/file', async (request, reply) => {\n const stream = createReadStream('./data.json');\n reply.type('application/json');\n return reply.send(stream);\n});\n\n// Streaming JSON array\napp.get('/stream', async (request, reply) => {\n reply.type('application/json');\n\n const cursor = db.users.findCursor();\n\n reply.raw.write('[');\n let first = true;\n\n for await (const user of cursor) {\n if (!first) reply.raw.write(',');\n reply.raw.write(JSON.stringify(user));\n first = false;\n }\n\n reply.raw.write(']');\n reply.raw.end();\n});\n```\n\n## Pre-Serialization Hook\n\nModify data before serialization:\n\n```typescript\napp.addHook('preSerialization', async (request, reply, payload) => {\n // Add metadata to responses\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n return {\n ...payload,\n _links: {\n self: request.url,\n },\n };\n }\n return payload;\n});\n```\n\n## Disable Serialization\n\nSkip serialization for specific routes:\n\n```typescript\napp.get('/raw', async (request, reply) => {\n const data = JSON.stringify({ raw: true });\n reply.type('application/json');\n reply.serializer((payload) => payload); // Pass through\n return data;\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10605,"content_sha256":"61c34b89811993d043a6064eb1dfe39c1e8560f3cad36745d37f0f3fd9952149"},{"filename":"rules/testing.md","content":"---\nname: testing\ndescription: Testing Fastify applications with inject()\nmetadata:\n tags: testing, inject, node-test, integration, unit\n---\n\n# Testing Fastify Applications\n\n## Contents\n\n- [Using inject() for Request Testing](#using-inject-for-request-testing)\n- [Testing with Headers and Authentication](#testing-with-headers-and-authentication)\n- [Testing Query Parameters](#testing-query-parameters)\n- [Testing URL Parameters](#testing-url-parameters)\n- [Testing Validation Errors](#testing-validation-errors)\n- [Testing File Uploads](#testing-file-uploads)\n- [Testing Streams](#testing-streams)\n- [Mocking Dependencies](#mocking-dependencies)\n- [Testing Plugins in Isolation](#testing-plugins-in-isolation)\n- [Testing Hooks](#testing-hooks)\n- [Test Factory Pattern](#test-factory-pattern)\n- [Database Testing with Transactions](#database-testing-with-transactions)\n- [Parallel Test Execution](#parallel-test-execution)\n- [Running Tests](#running-tests)\n\n## Using inject() for Request Testing\n\nFastify's `inject()` method simulates HTTP requests without network overhead:\n\n```typescript\nimport { describe, it, before, after } from 'node:test';\nimport Fastify from 'fastify';\nimport { buildApp } from './app.js';\n\ndescribe('User API', () => {\n let app;\n\n before(async () => {\n app = await buildApp();\n await app.ready();\n });\n\n after(async () => {\n await app.close();\n });\n\n it('should return users list', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/users',\n });\n\n t.assert.equal(response.statusCode, 200);\n t.assert.equal(response.headers['content-type'], 'application/json; charset=utf-8');\n\n const body = response.json();\n t.assert.ok(Array.isArray(body.users));\n });\n\n it('should create a user', async (t) => {\n const response = await app.inject({\n method: 'POST',\n url: '/users',\n payload: {\n name: 'John Doe',\n email: '[email protected]',\n },\n });\n\n t.assert.equal(response.statusCode, 201);\n\n const body = response.json();\n t.assert.equal(body.name, 'John Doe');\n t.assert.ok(body.id);\n });\n});\n```\n\n## Testing with Headers and Authentication\n\nTest authenticated endpoints:\n\n```typescript\ndescribe('Protected Routes', () => {\n let app;\n let authToken;\n\n before(async () => {\n app = await buildApp();\n await app.ready();\n\n // Get auth token\n const loginResponse = await app.inject({\n method: 'POST',\n url: '/auth/login',\n payload: {\n email: '[email protected]',\n password: 'password123',\n },\n });\n\n authToken = loginResponse.json().token;\n });\n\n after(async () => {\n await app.close();\n });\n\n it('should reject unauthenticated requests', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/profile',\n });\n\n t.assert.equal(response.statusCode, 401);\n });\n\n it('should return profile for authenticated user', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/profile',\n headers: {\n authorization: `Bearer ${authToken}`,\n },\n });\n\n t.assert.equal(response.statusCode, 200);\n t.assert.equal(response.json().email, '[email protected]');\n });\n});\n```\n\n## Testing Query Parameters\n\nTest routes with query strings:\n\n```typescript\nit('should filter users by status', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/users',\n query: {\n status: 'active',\n page: '1',\n limit: '10',\n },\n });\n\n t.assert.equal(response.statusCode, 200);\n const body = response.json();\n t.assert.ok(body.users.every((u) => u.status === 'active'));\n});\n\n// Or use URL with query string\nit('should search users', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/users?q=john&sort=name',\n });\n\n t.assert.equal(response.statusCode, 200);\n});\n```\n\n## Testing URL Parameters\n\nTest routes with path parameters:\n\n```typescript\nit('should return user by id', async (t) => {\n const userId = 'user-123';\n\n const response = await app.inject({\n method: 'GET',\n url: `/users/${userId}`,\n });\n\n t.assert.equal(response.statusCode, 200);\n t.assert.equal(response.json().id, userId);\n});\n\nit('should return 404 for non-existent user', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/users/non-existent',\n });\n\n t.assert.equal(response.statusCode, 404);\n});\n```\n\n## Testing Validation Errors\n\nTest schema validation:\n\n```typescript\ndescribe('Validation', () => {\n it('should reject invalid email', async (t) => {\n const response = await app.inject({\n method: 'POST',\n url: '/users',\n payload: {\n name: 'John',\n email: 'not-an-email',\n },\n });\n\n t.assert.equal(response.statusCode, 400);\n const body = response.json();\n t.assert.ok(body.message.includes('email'));\n });\n\n it('should reject missing required fields', async (t) => {\n const response = await app.inject({\n method: 'POST',\n url: '/users',\n payload: {\n name: 'John',\n // missing email\n },\n });\n\n t.assert.equal(response.statusCode, 400);\n });\n\n it('should coerce query parameters', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/items?limit=10&active=true',\n });\n\n t.assert.equal(response.statusCode, 200);\n // limit is coerced to number, active to boolean\n });\n});\n```\n\n## Testing File Uploads\n\nTest multipart form data:\n\n```typescript\nimport { createReadStream } from 'node:fs';\nimport FormData from 'form-data';\n\nit('should upload file', async (t) => {\n const form = new FormData();\n form.append('file', createReadStream('./test/fixtures/test.pdf'));\n form.append('name', 'test-document');\n\n const response = await app.inject({\n method: 'POST',\n url: '/upload',\n payload: form,\n headers: form.getHeaders(),\n });\n\n t.assert.equal(response.statusCode, 200);\n t.assert.ok(response.json().fileId);\n});\n```\n\n## Testing Streams\n\nTest streaming responses:\n\n```typescript\nit('should stream large file', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/files/large-file',\n });\n\n t.assert.equal(response.statusCode, 200);\n t.assert.ok(response.rawPayload.length > 0);\n});\n```\n\n## Mocking Dependencies\n\nMock external services and databases:\n\n```typescript\nimport { describe, it, before, after, mock } from 'node:test';\n\ndescribe('User Service', () => {\n let app;\n\n before(async () => {\n // Create app with mocked dependencies\n const mockDb = {\n users: {\n findAll: mock.fn(async () => [\n { id: '1', name: 'User 1' },\n { id: '2', name: 'User 2' },\n ]),\n findById: mock.fn(async (id) => {\n if (id === '1') return { id: '1', name: 'User 1' };\n return null;\n }),\n create: mock.fn(async (data) => ({ id: 'new-id', ...data })),\n },\n };\n\n app = Fastify();\n app.decorate('db', mockDb);\n app.register(import('./routes/users.js'));\n await app.ready();\n });\n\n after(async () => {\n await app.close();\n });\n\n it('should call findAll', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/users',\n });\n\n t.assert.equal(response.statusCode, 200);\n t.assert.equal(app.db.users.findAll.mock.calls.length, 1);\n });\n});\n```\n\n## Testing Plugins in Isolation\n\nTest plugins independently:\n\n```typescript\nimport { describe, it, before, after } from 'node:test';\nimport Fastify from 'fastify';\nimport cachePlugin from './plugins/cache.js';\n\ndescribe('Cache Plugin', () => {\n let app;\n\n before(async () => {\n app = Fastify();\n app.register(cachePlugin, { ttl: 1000 });\n await app.ready();\n });\n\n after(async () => {\n await app.close();\n });\n\n it('should decorate fastify with cache', (t) => {\n t.assert.ok(app.hasDecorator('cache'));\n t.assert.equal(typeof app.cache.get, 'function');\n t.assert.equal(typeof app.cache.set, 'function');\n });\n\n it('should cache and retrieve values', (t) => {\n app.cache.set('key', 'value');\n t.assert.equal(app.cache.get('key'), 'value');\n });\n});\n```\n\n## Testing Hooks\n\nTest hook behavior:\n\n```typescript\ndescribe('Hooks', () => {\n it('should add request id header', async (t) => {\n const response = await app.inject({\n method: 'GET',\n url: '/health',\n });\n\n t.assert.ok(response.headers['x-request-id']);\n });\n\n it('should log request timing', async (t) => {\n const logs = [];\n const app = Fastify({\n logger: {\n level: 'info',\n stream: {\n write: (msg) => logs.push(JSON.parse(msg)),\n },\n },\n });\n\n app.register(import('./app.js'));\n await app.ready();\n\n await app.inject({ method: 'GET', url: '/health' });\n\n const responseLog = logs.find((l) => l.msg?.includes('completed'));\n t.assert.ok(responseLog);\n t.assert.ok(responseLog.responseTime);\n\n await app.close();\n });\n});\n```\n\n## Test Factory Pattern\n\nCreate a reusable test app builder:\n\n```typescript\n// test/helper.ts\nimport Fastify from 'fastify';\nimport type { FastifyInstance } from 'fastify';\n\ninterface TestContext {\n app: FastifyInstance;\n inject: FastifyInstance['inject'];\n}\n\nexport async function buildTestApp(options = {}): Promise\u003cTestContext> {\n const app = Fastify({\n logger: false, // Disable logging in tests\n ...options,\n });\n\n // Register plugins\n app.register(import('../src/plugins/database.js'), {\n connectionString: process.env.TEST_DATABASE_URL,\n });\n app.register(import('../src/routes/index.js'));\n\n await app.ready();\n\n return {\n app,\n inject: app.inject.bind(app),\n };\n}\n\n// Usage in tests\ndescribe('API Tests', () => {\n let ctx: TestContext;\n\n before(async () => {\n ctx = await buildTestApp();\n });\n\n after(async () => {\n await ctx.app.close();\n });\n\n it('should work', async (t) => {\n const response = await ctx.inject({\n method: 'GET',\n url: '/health',\n });\n t.assert.equal(response.statusCode, 200);\n });\n});\n```\n\n## Database Testing with Transactions\n\nUse transactions for test isolation:\n\n```typescript\ndescribe('Database Integration', () => {\n let app;\n let transaction;\n\n before(async () => {\n app = await buildApp();\n await app.ready();\n });\n\n after(async () => {\n await app.close();\n });\n\n beforeEach(async () => {\n transaction = await app.db.beginTransaction();\n app.db.setTransaction(transaction);\n });\n\n afterEach(async () => {\n await transaction.rollback();\n });\n\n it('should create user', async (t) => {\n const response = await app.inject({\n method: 'POST',\n url: '/users',\n payload: { name: 'Test', email: '[email protected]' },\n });\n\n t.assert.equal(response.statusCode, 201);\n // Transaction is rolled back after test\n });\n});\n```\n\n## Parallel Test Execution\n\nStructure tests for parallel execution:\n\n```typescript\n// Tests run in parallel by default with node:test\n// Use separate app instances or proper isolation\n\nimport { describe, it } from 'node:test';\n\ndescribe('User API', async () => {\n // Each test suite gets its own app instance\n const app = await buildTestApp();\n\n it('test 1', async (t) => {\n // ...\n });\n\n it('test 2', async (t) => {\n // ...\n });\n\n // Cleanup after all tests in this suite\n after(() => app.close());\n});\n\ndescribe('Post API', async () => {\n const app = await buildTestApp();\n\n it('test 1', async (t) => {\n // ...\n });\n\n after(() => app.close());\n});\n```\n\n## Running Tests\n\n```bash\n# Run all tests\nnode --test\n\n# Run with TypeScript\nnode --test src/**/*.test.ts\n\n# Run specific file\nnode --test src/routes/users.test.ts\n\n# With coverage\nnode --test --experimental-test-coverage\n\n# Watch mode\nnode --test --watch\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11834,"content_sha256":"84b1a8b78b876f2d7d4f182258237ef098c2acdaa9f803968110d377adeb54c1"},{"filename":"rules/typescript.md","content":"---\nname: typescript\ndescription: TypeScript integration with Fastify\nmetadata:\n tags: typescript, types, generics, type-safety\n---\n\n# TypeScript Integration\n\n## Contents\n\n- [Type Stripping with Node.js](#type-stripping-with-nodejs)\n- [Basic Type Safety](#basic-type-safety)\n- [Typing Route Handlers](#typing-route-handlers)\n- [Type Providers](#type-providers)\n- [Typing Decorators](#typing-decorators)\n- [Typing Plugins](#typing-plugins)\n- [Typing Hooks](#typing-hooks)\n- [Typing Schema Objects](#typing-schema-objects)\n- [Shared Types](#shared-types)\n- [Type-Safe Route Registration](#type-safe-route-registration)\n- [Avoiding Type Gymnastics](#avoiding-type-gymnastics)\n- [Type Checking Without Compilation](#type-checking-without-compilation)\n\n## Type Stripping with Node.js\n\nUse Node.js built-in type stripping (Node.js 22.6+):\n\n```bash\n# Run TypeScript directly\nnode --experimental-strip-types app.ts\n\n# In Node.js 23+\nnode app.ts\n```\n\n```json\n// package.json\n{\n \"type\": \"module\",\n \"scripts\": {\n \"start\": \"node app.ts\",\n \"dev\": \"node --watch app.ts\"\n }\n}\n```\n\n```typescript\n// tsconfig.json for type stripping\n{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"verbatimModuleSyntax\": true,\n \"erasableSyntaxOnly\": true,\n \"noEmit\": true,\n \"strict\": true\n }\n}\n```\n\n## Basic Type Safety\n\nType your Fastify application:\n\n```typescript\nimport Fastify, { type FastifyInstance, type FastifyRequest, type FastifyReply } from 'fastify';\n\nconst app: FastifyInstance = Fastify({ logger: true });\n\napp.get('/health', async (request: FastifyRequest, reply: FastifyReply) => {\n return { status: 'ok' };\n});\n\nawait app.listen({ port: 3000 });\n```\n\n## Typing Route Handlers\n\nUse generics to type request parts:\n\n```typescript\nimport type { FastifyRequest, FastifyReply } from 'fastify';\n\ninterface CreateUserBody {\n name: string;\n email: string;\n}\n\ninterface UserParams {\n id: string;\n}\n\ninterface UserQuery {\n include?: string;\n}\n\n// Type the request with generics\napp.post\u003c{\n Body: CreateUserBody;\n}>('/users', async (request, reply) => {\n const { name, email } = request.body; // Fully typed\n return { name, email };\n});\n\napp.get\u003c{\n Params: UserParams;\n Querystring: UserQuery;\n}>('/users/:id', async (request) => {\n const { id } = request.params; // string\n const { include } = request.query; // string | undefined\n return { id, include };\n});\n\n// Full route options typing\napp.route\u003c{\n Params: UserParams;\n Querystring: UserQuery;\n Body: CreateUserBody;\n Reply: { user: { id: string; name: string } };\n}>({\n method: 'PUT',\n url: '/users/:id',\n handler: async (request, reply) => {\n return { user: { id: request.params.id, name: request.body.name } };\n },\n});\n```\n\n## Type Providers\n\nUse @fastify/type-provider-typebox for runtime + compile-time safety:\n\n```typescript\nimport Fastify from 'fastify';\nimport { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';\nimport { Type } from '@sinclair/typebox';\n\nconst app = Fastify().withTypeProvider\u003cTypeBoxTypeProvider>();\n\nconst UserSchema = Type.Object({\n id: Type.String(),\n name: Type.String(),\n email: Type.String({ format: 'email' }),\n});\n\nconst CreateUserSchema = Type.Object({\n name: Type.String({ minLength: 1 }),\n email: Type.String({ format: 'email' }),\n});\n\napp.post('/users', {\n schema: {\n body: CreateUserSchema,\n response: {\n 201: UserSchema,\n },\n },\n}, async (request, reply) => {\n // request.body is typed as { name: string; email: string }\n const { name, email } = request.body;\n\n reply.code(201);\n return { id: 'generated', name, email };\n});\n```\n\n## Typing Decorators\n\nExtend Fastify types with declaration merging:\n\n```typescript\nimport Fastify from 'fastify';\n\n// Declare types for decorators\ndeclare module 'fastify' {\n interface FastifyInstance {\n config: {\n port: number;\n host: string;\n };\n db: Database;\n }\n\n interface FastifyRequest {\n user?: {\n id: string;\n email: string;\n role: string;\n };\n startTime: number;\n }\n\n interface FastifyReply {\n sendSuccess: (data: unknown) => void;\n }\n}\n\nconst app = Fastify();\n\n// Add decorators\napp.decorate('config', { port: 3000, host: 'localhost' });\napp.decorate('db', new Database());\n\napp.decorateRequest('user', null);\napp.decorateRequest('startTime', 0);\n\napp.decorateReply('sendSuccess', function (data: unknown) {\n this.send({ success: true, data });\n});\n\n// Now fully typed\napp.get('/profile', async (request, reply) => {\n const user = request.user; // { id: string; email: string; role: string } | undefined\n const config = app.config; // { port: number; host: string }\n\n reply.sendSuccess({ user });\n});\n```\n\n## Typing Plugins\n\nType plugin options and exports:\n\n```typescript\nimport fp from 'fastify-plugin';\nimport type { FastifyPluginAsync } from 'fastify';\n\ninterface DatabasePluginOptions {\n connectionString: string;\n poolSize?: number;\n}\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n db: {\n query: (sql: string, params?: unknown[]) => Promise\u003cunknown[]>;\n close: () => Promise\u003cvoid>;\n };\n }\n}\n\nconst databasePlugin: FastifyPluginAsync\u003cDatabasePluginOptions> = async (\n fastify,\n options,\n) => {\n const { connectionString, poolSize = 10 } = options;\n\n const db = await createConnection(connectionString, poolSize);\n\n fastify.decorate('db', {\n query: (sql: string, params?: unknown[]) => db.query(sql, params),\n close: () => db.end(),\n });\n\n fastify.addHook('onClose', async () => {\n await db.end();\n });\n};\n\nexport default fp(databasePlugin, {\n name: 'database',\n});\n```\n\n## Typing Hooks\n\nType hook functions:\n\n```typescript\nimport type {\n FastifyRequest,\n FastifyReply,\n onRequestHookHandler,\n preHandlerHookHandler,\n} from 'fastify';\n\nconst authHook: preHandlerHookHandler = async (\n request: FastifyRequest,\n reply: FastifyReply,\n) => {\n const token = request.headers.authorization;\n if (!token) {\n reply.code(401).send({ error: 'Unauthorized' });\n return;\n }\n request.user = await verifyToken(token);\n};\n\nconst timingHook: onRequestHookHandler = async (request) => {\n request.startTime = Date.now();\n};\n\napp.addHook('onRequest', timingHook);\napp.addHook('preHandler', authHook);\n```\n\n## Typing Schema Objects\n\nCreate reusable typed schemas:\n\n```typescript\nimport type { JSONSchema7 } from 'json-schema';\n\n// Define schema with const assertion for type inference\nconst userSchema = {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n email: { type: 'string', format: 'email' },\n },\n required: ['id', 'name', 'email'],\n} as const satisfies JSONSchema7;\n\n// Infer TypeScript type from schema\ntype User = {\n id: string;\n name: string;\n email: string;\n};\n\napp.get\u003c{ Reply: User }>('/users/:id', {\n schema: {\n response: {\n 200: userSchema,\n },\n },\n}, async (request) => {\n return { id: '1', name: 'John', email: '[email protected]' };\n});\n```\n\n## Shared Types\n\nOrganize types in dedicated files:\n\n```typescript\n// types/index.ts\nexport interface User {\n id: string;\n name: string;\n email: string;\n role: 'admin' | 'user';\n}\n\nexport interface CreateUserInput {\n name: string;\n email: string;\n}\n\nexport interface PaginationQuery {\n page?: number;\n limit?: number;\n sort?: string;\n}\n\n// routes/users.ts\nimport type { FastifyInstance } from 'fastify';\nimport type { User, CreateUserInput, PaginationQuery } from '../types/index.js';\n\nexport default async function userRoutes(fastify: FastifyInstance) {\n fastify.get\u003c{\n Querystring: PaginationQuery;\n Reply: { users: User[]; total: number };\n }>('/', async (request) => {\n const { page = 1, limit = 10 } = request.query;\n // ...\n });\n\n fastify.post\u003c{\n Body: CreateUserInput;\n Reply: User;\n }>('/', async (request, reply) => {\n reply.code(201);\n // ...\n });\n}\n```\n\n## Type-Safe Route Registration\n\nCreate typed route factories:\n\n```typescript\nimport type { FastifyInstance, RouteOptions } from 'fastify';\n\nfunction createCrudRoutes\u003cT extends { id: string }>(\n fastify: FastifyInstance,\n options: {\n prefix: string;\n schema: {\n item: object;\n create: object;\n update: object;\n };\n handlers: {\n list: () => Promise\u003cT[]>;\n get: (id: string) => Promise\u003cT | null>;\n create: (data: unknown) => Promise\u003cT>;\n update: (id: string, data: unknown) => Promise\u003cT>;\n delete: (id: string) => Promise\u003cvoid>;\n };\n },\n) {\n const { prefix, schema, handlers } = options;\n\n fastify.get(`${prefix}`, {\n schema: { response: { 200: { type: 'array', items: schema.item } } },\n }, async () => handlers.list());\n\n fastify.get(`${prefix}/:id`, {\n schema: { response: { 200: schema.item } },\n }, async (request) => {\n const item = await handlers.get((request.params as { id: string }).id);\n if (!item) throw { statusCode: 404, message: 'Not found' };\n return item;\n });\n\n // ... more routes\n}\n```\n\n## Avoiding Type Gymnastics\n\nKeep types simple and practical:\n\n```typescript\n// GOOD - simple, readable types\ninterface UserRequest {\n Params: { id: string };\n Body: { name: string };\n}\n\napp.put\u003cUserRequest>('/users/:id', handler);\n\n// AVOID - overly complex generic types\ntype DeepPartial\u003cT> = T extends object ? {\n [P in keyof T]?: DeepPartial\u003cT[P]>;\n} : T;\n\n// AVOID - excessive type inference\ntype InferSchemaType\u003cT> = T extends { properties: infer P }\n ? { [K in keyof P]: InferPropertyType\u003cP[K]> }\n : never;\n```\n\n## Type Checking Without Compilation\n\nUse TypeScript for type checking only:\n\n```bash\n# Type check without emitting\nnpx tsc --noEmit\n\n# Watch mode\nnpx tsc --noEmit --watch\n\n# In CI\nnpm run typecheck\n```\n\n```json\n// package.json\n{\n \"scripts\": {\n \"start\": \"node app.ts\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"npm run typecheck && node --test\"\n }\n}\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9879,"content_sha256":"4ce29b8786ad844494a61b6b2e45436c507c8997bcdad8298dd3b7356ceb1ad3"},{"filename":"rules/websockets.md","content":"---\nname: websockets\ndescription: WebSocket support in Fastify\nmetadata:\n tags: websockets, realtime, ws, socket\n---\n\n# WebSocket Support\n\n## Contents\n\n- [Using @fastify/websocket](#using-fastifywebsocket)\n- [WebSocket with Hooks](#websocket-with-hooks)\n- [Connection Options](#connection-options)\n- [Broadcast to All Clients](#broadcast-to-all-clients)\n- [Rooms/Channels Pattern](#roomschannels-pattern)\n- [Structured Message Protocol](#structured-message-protocol)\n- [Heartbeat/Ping-Pong](#heartbeatping-pong)\n- [Authentication](#authentication)\n- [Error Handling](#error-handling)\n- [Rate Limiting WebSocket Messages](#rate-limiting-websocket-messages)\n- [Graceful Shutdown](#graceful-shutdown)\n- [Full-Duplex Stream Pattern](#full-duplex-stream-pattern)\n\n## Using @fastify/websocket\n\nAdd WebSocket support to Fastify:\n\n```typescript\nimport Fastify from 'fastify';\nimport websocket from '@fastify/websocket';\n\nconst app = Fastify();\n\napp.register(websocket);\n\napp.get('/ws', { websocket: true }, (socket, request) => {\n socket.on('message', (message) => {\n const data = message.toString();\n console.log('Received:', data);\n\n // Echo back\n socket.send(`Echo: ${data}`);\n });\n\n socket.on('close', () => {\n console.log('Client disconnected');\n });\n\n socket.on('error', (error) => {\n console.error('WebSocket error:', error);\n });\n});\n\nawait app.listen({ port: 3000 });\n```\n\n## WebSocket with Hooks\n\nUse Fastify hooks with WebSocket routes:\n\n```typescript\napp.register(async function wsRoutes(fastify) {\n // This hook runs before WebSocket upgrade\n fastify.addHook('preValidation', async (request, reply) => {\n const token = request.headers.authorization;\n if (!token) {\n reply.code(401).send({ error: 'Unauthorized' });\n return;\n }\n request.user = await verifyToken(token);\n });\n\n fastify.get('/ws', { websocket: true }, (socket, request) => {\n console.log('Connected user:', request.user.id);\n\n socket.on('message', (message) => {\n // Handle authenticated messages\n });\n });\n});\n```\n\n## Connection Options\n\nConfigure WebSocket server options:\n\n```typescript\napp.register(websocket, {\n options: {\n maxPayload: 1048576, // 1MB max message size\n clientTracking: true,\n perMessageDeflate: {\n zlibDeflateOptions: {\n chunkSize: 1024,\n memLevel: 7,\n level: 3,\n },\n zlibInflateOptions: {\n chunkSize: 10 * 1024,\n },\n },\n },\n});\n```\n\n## Broadcast to All Clients\n\nBroadcast messages to connected clients:\n\n```typescript\nconst clients = new Set\u003cWebSocket>();\n\napp.get('/ws', { websocket: true }, (socket, request) => {\n clients.add(socket);\n\n socket.on('close', () => {\n clients.delete(socket);\n });\n\n socket.on('message', (message) => {\n // Broadcast to all other clients\n for (const client of clients) {\n if (client !== socket && client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n }\n });\n});\n\n// Broadcast from HTTP route\napp.post('/broadcast', async (request) => {\n const { message } = request.body;\n\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(JSON.stringify({ type: 'broadcast', message }));\n }\n }\n\n return { sent: clients.size };\n});\n```\n\n## Rooms/Channels Pattern\n\nOrganize connections into rooms:\n\n```typescript\nconst rooms = new Map\u003cstring, Set\u003cWebSocket>>();\n\nfunction joinRoom(socket: WebSocket, roomId: string) {\n if (!rooms.has(roomId)) {\n rooms.set(roomId, new Set());\n }\n rooms.get(roomId)!.add(socket);\n}\n\nfunction leaveRoom(socket: WebSocket, roomId: string) {\n rooms.get(roomId)?.delete(socket);\n if (rooms.get(roomId)?.size === 0) {\n rooms.delete(roomId);\n }\n}\n\nfunction broadcastToRoom(roomId: string, message: string, exclude?: WebSocket) {\n const room = rooms.get(roomId);\n if (!room) return;\n\n for (const client of room) {\n if (client !== exclude && client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n }\n}\n\napp.get('/ws/:roomId', { websocket: true }, (socket, request) => {\n const { roomId } = request.params as { roomId: string };\n\n joinRoom(socket, roomId);\n\n socket.on('message', (message) => {\n broadcastToRoom(roomId, message.toString(), socket);\n });\n\n socket.on('close', () => {\n leaveRoom(socket, roomId);\n });\n});\n```\n\n## Structured Message Protocol\n\nUse JSON for structured messages:\n\n```typescript\ninterface WSMessage {\n type: string;\n payload?: unknown;\n id?: string;\n}\n\napp.get('/ws', { websocket: true }, (socket, request) => {\n function send(message: WSMessage) {\n socket.send(JSON.stringify(message));\n }\n\n socket.on('message', (raw) => {\n let message: WSMessage;\n\n try {\n message = JSON.parse(raw.toString());\n } catch {\n send({ type: 'error', payload: 'Invalid JSON' });\n return;\n }\n\n switch (message.type) {\n case 'ping':\n send({ type: 'pong', id: message.id });\n break;\n\n case 'subscribe':\n handleSubscribe(socket, message.payload);\n send({ type: 'subscribed', payload: message.payload, id: message.id });\n break;\n\n case 'message':\n handleMessage(socket, message.payload);\n break;\n\n default:\n send({ type: 'error', payload: 'Unknown message type' });\n }\n });\n});\n```\n\n## Heartbeat/Ping-Pong\n\nKeep connections alive:\n\n```typescript\nconst HEARTBEAT_INTERVAL = 30000;\nconst clients = new Map\u003cWebSocket, { isAlive: boolean }>();\n\napp.get('/ws', { websocket: true }, (socket, request) => {\n clients.set(socket, { isAlive: true });\n\n socket.on('pong', () => {\n const client = clients.get(socket);\n if (client) client.isAlive = true;\n });\n\n socket.on('close', () => {\n clients.delete(socket);\n });\n});\n\n// Heartbeat interval\nsetInterval(() => {\n for (const [socket, state] of clients) {\n if (!state.isAlive) {\n socket.terminate();\n clients.delete(socket);\n continue;\n }\n\n state.isAlive = false;\n socket.ping();\n }\n}, HEARTBEAT_INTERVAL);\n```\n\n## Authentication\n\nAuthenticate WebSocket connections:\n\n```typescript\napp.get('/ws', {\n websocket: true,\n preValidation: async (request, reply) => {\n // Authenticate via query parameter or header\n const token = request.query.token || request.headers.authorization?.replace('Bearer ', '');\n\n if (!token) {\n reply.code(401).send({ error: 'Token required' });\n return;\n }\n\n try {\n request.user = await verifyToken(token);\n } catch {\n reply.code(401).send({ error: 'Invalid token' });\n }\n },\n}, (socket, request) => {\n console.log('Authenticated user:', request.user);\n\n socket.on('message', (message) => {\n // Handle authenticated messages\n });\n});\n```\n\n## Error Handling\n\nHandle WebSocket errors properly:\n\n```typescript\napp.get('/ws', { websocket: true }, (socket, request) => {\n socket.on('error', (error) => {\n request.log.error({ err: error }, 'WebSocket error');\n });\n\n socket.on('message', async (raw) => {\n try {\n const message = JSON.parse(raw.toString());\n const result = await processMessage(message);\n socket.send(JSON.stringify({ success: true, result }));\n } catch (error) {\n request.log.error({ err: error }, 'Message processing error');\n socket.send(JSON.stringify({\n success: false,\n error: error.message,\n }));\n }\n });\n});\n```\n\n## Rate Limiting WebSocket Messages\n\nLimit message frequency:\n\n```typescript\nconst rateLimits = new Map\u003cWebSocket, { count: number; resetAt: number }>();\n\nfunction checkRateLimit(socket: WebSocket, limit: number, window: number): boolean {\n const now = Date.now();\n let state = rateLimits.get(socket);\n\n if (!state || now > state.resetAt) {\n state = { count: 0, resetAt: now + window };\n rateLimits.set(socket, state);\n }\n\n state.count++;\n\n if (state.count > limit) {\n return false;\n }\n\n return true;\n}\n\napp.get('/ws', { websocket: true }, (socket, request) => {\n socket.on('message', (message) => {\n if (!checkRateLimit(socket, 100, 60000)) {\n socket.send(JSON.stringify({ error: 'Rate limit exceeded' }));\n return;\n }\n\n // Process message\n });\n\n socket.on('close', () => {\n rateLimits.delete(socket);\n });\n});\n```\n\n## Graceful Shutdown\n\nClose WebSocket connections on shutdown:\n\n```typescript\nimport closeWithGrace from 'close-with-grace';\n\nconst connections = new Set\u003cWebSocket>();\n\napp.get('/ws', { websocket: true }, (socket, request) => {\n connections.add(socket);\n\n socket.on('close', () => {\n connections.delete(socket);\n });\n});\n\ncloseWithGrace({ delay: 5000 }, async ({ signal }) => {\n // Notify clients\n for (const socket of connections) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ type: 'shutdown', message: 'Server is shutting down' }));\n socket.close(1001, 'Server shutdown');\n }\n }\n\n await app.close();\n});\n```\n\n## Full-Duplex Stream Pattern\n\nUse WebSocket for streaming data:\n\n```typescript\napp.get('/ws/stream', { websocket: true }, async (socket, request) => {\n const stream = createDataStream();\n\n stream.on('data', (data) => {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ type: 'data', payload: data }));\n }\n });\n\n stream.on('end', () => {\n socket.send(JSON.stringify({ type: 'end' }));\n socket.close();\n });\n\n socket.on('message', (message) => {\n const { type, payload } = JSON.parse(message.toString());\n\n if (type === 'pause') {\n stream.pause();\n } else if (type === 'resume') {\n stream.resume();\n }\n });\n\n socket.on('close', () => {\n stream.destroy();\n });\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9649,"content_sha256":"45def95a6c73b8d5c467c9ff36e0ec67bad8c6c42111f0124ab730f5651b8fe5"},{"filename":"tile.json","content":"{\n \"name\": \"mcollina/fastify-best-practices\",\n \"version\": \"0.1.0\",\n \"private\": false,\n \"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.\",\n \"skills\": {\n \"fastify-best-practices\": {\n \"path\": \"SKILL.md\"\n }\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":869,"content_sha256":"f400e91da712b3522b65f50db35415e8eea6d889abf5339230cdee8a0beb04fb"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":2},"content":[{"text":"When to use","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill when you need to:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Develop backend applications using Fastify","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement Fastify plugins and route handlers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Get guidance on Fastify architecture and patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use TypeScript with Fastify (strip types)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement testing with Fastify's inject method","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Configure validation, serialization, and error handling","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start","type":"text"}]},{"type":"paragraph","content":[{"text":"A minimal, runnable Fastify server to get started immediately:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"import Fastify from 'fastify'\n\nconst app = Fastify({ logger: true })\n\napp.get('/health', async (request, reply) => {\n return { status: 'ok' }\n})\n\nconst start = async () => {\n await app.listen({ port: 3000, host: '0.0.0.0' })\n}\nstart()","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Recommended Reading Order for Common Scenarios","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"New to Fastify?","type":"text","marks":[{"type":"strong"}]},{"text":" Start with ","type":"text"},{"text":"plugins.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"routes.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"schemas.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding authentication:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"plugins.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"hooks.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"authentication.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Improving performance:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"schemas.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"serialization.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"performance.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setting up testing:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"routes.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"testing.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Going to production:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"logging.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"configuration.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"deployment.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to use","type":"text"}]},{"type":"paragraph","content":[{"text":"Read individual rule files for detailed explanations and code examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/plugins.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/plugins.md","title":null}}]},{"text":" - Plugin development and encapsulation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/routes.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/routes.md","title":null}}]},{"text":" - Route organization and handlers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/schemas.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/schemas.md","title":null}}]},{"text":" - JSON Schema validation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/error-handling.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/error-handling.md","title":null}}]},{"text":" - Error handling patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/hooks.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/hooks.md","title":null}}]},{"text":" - Hooks and request lifecycle","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/authentication.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/authentication.md","title":null}}]},{"text":" - Authentication and authorization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/testing.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/testing.md","title":null}}]},{"text":" - Testing with inject()","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/performance.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/performance.md","title":null}}]},{"text":" - Performance optimization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/logging.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/logging.md","title":null}}]},{"text":" - Logging with Pino","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/typescript.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/typescript.md","title":null}}]},{"text":" - TypeScript integration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/decorators.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/decorators.md","title":null}}]},{"text":" - Decorators and extensions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/content-type.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/content-type.md","title":null}}]},{"text":" - Content type parsing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/serialization.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/serialization.md","title":null}}]},{"text":" - Response serialization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/cors-security.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/cors-security.md","title":null}}]},{"text":" - CORS and security headers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/websockets.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/websockets.md","title":null}}]},{"text":" - WebSocket support","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/database.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/database.md","title":null}}]},{"text":" - Database integration patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/configuration.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/configuration.md","title":null}}]},{"text":" - Application configuration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/deployment.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/deployment.md","title":null}}]},{"text":" - Production deployment","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rules/http-proxy.md","type":"text","marks":[{"type":"link","attrs":{"href":"rules/http-proxy.md","title":null}}]},{"text":" - HTTP proxying and reply.from()","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Principles","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Encapsulation","type":"text","marks":[{"type":"strong"}]},{"text":": Fastify's plugin system provides automatic encapsulation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Schema-first","type":"text","marks":[{"type":"strong"}]},{"text":": Define schemas for validation and serialization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Performance","type":"text","marks":[{"type":"strong"}]},{"text":": Fastify is optimized for speed; use its features correctly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Async/await","type":"text","marks":[{"type":"strong"}]},{"text":": All handlers and hooks support async functions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Minimal dependencies","type":"text","marks":[{"type":"strong"}]},{"text":": Prefer Fastify's built-in features and official plugins","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"fastify-best-practices","tags":["fastify","nodejs","typescript","backend","api","server","http"],"author":"@skillopedia","source":{"stars":1961,"repo_name":"skills","origin_url":"https://github.com/mcollina/skills/blob/HEAD/skills/fastify/SKILL.md","repo_owner":"mcollina","body_sha256":"d1d90340de01fe4b13aadcc868e69a15f98bf03d1dd881bacbb4762b93e93733","cluster_key":"351417b89b466e1091b16bd488d5205e1203499c2785c13f9e214c9c01c4aacd","clean_bundle":{"format":"clean-skill-bundle-v1","source":"mcollina/skills/skills/fastify/SKILL.md","text_bytes":186408,"source_commit":"c75d70dbfbe991d8cec42aeda0b64ff14fbb9ac7","skills_sh_rank":562,"attachment_count":20,"text_attachments":20,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/fastify/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"software-engineering","category_label":"Engineering"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"software-engineering","metadata":{"tags":"fastify, nodejs, typescript, backend, api, server, http"},"import_tag":"skills-sh-top20k-2026-11","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."}},"renderedAt":1793661656491}

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: Recommended Reading Order for Common Scenarios - New to Fastify? Start with → → - Adding authentication: → → - Improving performance: → → - Setting up testing: → - Going to production:…