Claude Skill

api-framework-fastify

Fastify routes, JSON Schema validation, plugin system, TypeScript type providers

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-framework-fastify_skills_api-framework-fastify-3a51ef5.zip · 22 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-framework-fastify/skills/api-framework-fastify
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

API Development with Fastify

Quick Guide: Use Fastify for high-performance Node.js REST APIs with built-in JSON Schema validation and powerful plugin encapsulation. Use @fastify/type-provider-typebox for end-to-end type safety (both Type and TypeBoxTypeProvider re-exported from it). Wrap shared plugins with fastify-plugin to expose decorators. Always define response schemas for serialization performance and data leak prevention.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST use withTypeProvider<>() for type-safe request/response handling)

(You MUST wrap shared plugins with fastify-plugin to expose decorators to parent scope)

(You MUST define response schemas to enable fast-json-stringify optimization)

(You MUST use named constants for HTTP status codes - never raw numbers)

</critical_requirements>


Auto-detection: Fastify, fastify.register, fastify.decorate, fastify-plugin, TypeBox, @fastify/type-provider-typebox, @fastify/type-provider-json-schema-to-ts, fastify-type-provider-zod, preHandler, onRequest, preSerialization, JSON Schema validation, fast-json-stringify, FastifyPluginAsyncTypebox

When to use:

  • Building high-performance REST APIs (45k+ req/sec benchmarks)
  • Need schema-based validation with automatic coercion
  • Want plugin encapsulation for modular architecture
  • Require lifecycle hooks for cross-cutting concerns
  • Building APIs with strict TypeScript type safety requirements

When NOT to use:

  • Simple internal APIs without performance requirements (consider your existing solution)
  • GraphQL APIs (use dedicated GraphQL servers)
  • Edge/serverless with size constraints (Fastify has larger footprint than minimal frameworks)
  • When middleware ecosystem compatibility with Express is required

Key patterns covered:

  • Server setup with TypeScript type providers
  • Plugin system and encapsulation patterns
  • JSON Schema validation for request/response
  • Lifecycle hooks (onRequest, preHandler, onSend, etc.)
  • Decorators for extending Fastify/Request/Reply
  • Error handling with setErrorHandler
  • Route organization with prefix patterns

Detailed Resources:




<red_flags>

RED FLAGS

High Priority Issues

  • No type provider configured - Loses compile-time type safety on request/response
  • Shared plugins without fastify-plugin - Decorators invisible to other plugins
  • Missing response schemas - Loses 2-3x serialization performance AND risks data leaks
  • Raw status code numbers - Use named constants (HTTP_OK, HTTP_NOT_FOUND)
  • Reference types in decorateRequest/decorateReply - Shared mutable state across ALL requests (security risk)

Medium Priority Issues

  • No error handler configured - Stack traces exposed to clients in production
  • Missing dependencies in plugin options - Race conditions on decorator access
  • No schema for query/params - No validation, types are unknown
  • Inline route handlers in god files - Use modular route plugins with prefix

Common Mistakes

  • Forgetting await server.ready() - Plugins may not be fully loaded
  • Not cleaning up in onClose - Connection leaks on shutdown
  • Mixing async/await with done callback - Pick one pattern per hook (causes double-completion)
  • Using Express patterns - res.send() vs reply.send(), next() vs returning

Gotchas & Edge Cases

  • Hook return values: Returning a value from hooks sends response immediately (short-circuits)
  • Plugin registration order: Later plugins can't access earlier encapsulated decorators
  • Validation error shape: Fastify validation errors have .validation array, not .message
  • Route specificity: More specific routes must be registered before wildcards
  • preHandler order: Route-level runs AFTER plugin-level hooks
  • onResponse timing: Runs after response sent, cannot modify response
  • Schema compilation: Happens at startup, errors surface during server.ready()
  • v5 redirect order: reply.redirect(url, statusCode) not reply.redirect(statusCode, url) (reversed from v4)
  • v5 reply.sent: Use reply.hijack() instead of setting reply.sent = true

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST use withTypeProvider<>() for type-safe request/response handling)

(You MUST wrap shared plugins with fastify-plugin to expose decorators to parent scope)

(You MUST define response schemas to enable fast-json-stringify optimization)

(You MUST use named constants for HTTP status codes - never raw numbers)

Failure to follow these rules will break type safety and lose performance benefits.

</critical_reminders>

Files (skills)
  • examples
    • core.md 9.3 KB
      # Fastify - Core Examples
      
      > Essential patterns for server setup, route definition, error handling, and testing. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Prerequisites**: None - start here.
      
      ---
      
      ## Pattern 1: Server Setup with Type Provider
      
      ### Good Example - Factory Pattern with TypeBox
      
      ```typescript
      // src/server.ts
      import Fastify from "fastify";
      import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
      
      import { errorHandler } from "./plugins/error-handler";
      import { userRoutes } from "./routes/users";
      
      const SERVER_PORT = 3000;
      const SERVER_HOST = "0.0.0.0";
      
      const buildServer = () => {
        const server = Fastify({
          logger: {
            level: process.env.LOG_LEVEL ?? "info",
          },
        }).withTypeProvider<TypeBoxTypeProvider>();
      
        // Register global error handler
        server.setErrorHandler(errorHandler);
      
        // Register route plugins with prefixes
        server.register(userRoutes, { prefix: "/api/users" });
      
        return server;
      };
      
      const start = async () => {
        const server = buildServer();
      
        try {
          await server.listen({ port: SERVER_PORT, host: SERVER_HOST });
        } catch (error) {
          server.log.error(error);
          process.exit(1);
        }
      };
      
      // Named exports
      export { buildServer, start };
      ```
      
      **Why good:** TypeBox provider enables type inference from schemas, factory function enables testing, logger configured from environment
      
      ### Bad Example - No Type Provider
      
      ```typescript
      // WRONG: No type provider, inline configuration
      import Fastify from "fastify";
      
      const server = Fastify();
      
      server.get("/users", async (req, reply) => {
        const limit = req.query.limit; // any type!
        return { users: [] };
      });
      
      server.listen({ port: 3000 });
      ```
      
      **Why bad:** No type safety on request/response, magic number for port, no error handling for startup
      
      ---
      
      ## Pattern 2: Route Definition with Full Schema
      
      ### Good Example - Complete CRUD Routes
      
      ```typescript
      // src/routes/users.ts
      import type { FastifyPluginAsync } from "fastify";
      import { Type } from "@fastify/type-provider-typebox";
      
      import {
        UserSchema,
        CreateUserSchema,
        UserParamsSchema,
        UsersQuerySchema,
      } from "../schemas/user";
      
      const HTTP_OK = 200;
      const HTTP_CREATED = 201;
      const HTTP_NOT_FOUND = 404;
      
      const ErrorSchema = Type.Object({
        statusCode: Type.Integer(),
        error: Type.String(),
        message: Type.String(),
      });
      
      export const userRoutes: FastifyPluginAsync = async (fastify) => {
        // GET /api/users
        fastify.get(
          "/",
          {
            schema: {
              querystring: UsersQuerySchema,
              response: {
                [HTTP_OK]: Type.Object({
                  users: Type.Array(UserSchema),
                  total: Type.Integer(),
                }),
              },
            },
          },
          async (request, reply) => {
            const { limit, offset } = request.query;
            // request.query is typed as UsersQuery
      
            const users = await fastify.userService.list({ limit, offset });
      
            return reply.status(HTTP_OK).send({
              users,
              total: users.length,
            });
          },
        );
      
        // GET /api/users/:id
        fastify.get(
          "/:id",
          {
            schema: {
              params: UserParamsSchema,
              response: {
                [HTTP_OK]: UserSchema,
                [HTTP_NOT_FOUND]: ErrorSchema,
              },
            },
          },
          async (request, reply) => {
            const { id } = request.params;
            // request.params is typed as UserParams
      
            const user = await fastify.userService.findById(id);
      
            if (!user) {
              return reply.status(HTTP_NOT_FOUND).send({
                statusCode: HTTP_NOT_FOUND,
                error: "Not Found",
                message: `User ${id} not found`,
              });
            }
      
            return reply.status(HTTP_OK).send(user);
          },
        );
      
        // POST /api/users
        fastify.post(
          "/",
          {
            schema: {
              body: CreateUserSchema,
              response: {
                [HTTP_CREATED]: UserSchema,
              },
            },
          },
          async (request, reply) => {
            const userData = request.body;
            // request.body is typed as CreateUser
      
            const user = await fastify.userService.create(userData);
      
            return reply.status(HTTP_CREATED).send(user);
          },
        );
      };
      ```
      
      **Why good:** Response schemas enable fast-json-stringify optimization (2-3x faster), full type inference on request objects, HTTP constants prevent magic numbers
      
      ---
      
      ## Pattern 3: Error Handling
      
      ### Good Example - Centralized Error Handler
      
      ```typescript
      // src/plugins/error-handler.ts
      import type { FastifyError, FastifyReply, FastifyRequest } from "fastify";
      
      const HTTP_BAD_REQUEST = 400;
      const HTTP_NOT_FOUND = 404;
      const HTTP_INTERNAL_ERROR = 500;
      
      // Custom error classes
      export class NotFoundError extends Error {
        statusCode = HTTP_NOT_FOUND;
      
        constructor(resource: string, id: string) {
          super(`${resource} with id ${id} not found`);
          this.name = "NotFoundError";
        }
      }
      
      export class ValidationError extends Error {
        statusCode = HTTP_BAD_REQUEST;
        details: unknown;
      
        constructor(message: string, details?: unknown) {
          super(message);
          this.name = "ValidationError";
          this.details = details;
        }
      }
      
      export const errorHandler = (
        error: FastifyError,
        request: FastifyRequest,
        reply: FastifyReply,
      ) => {
        // Fastify validation errors
        if (error.validation) {
          request.log.warn(
            { reqId: request.id, validation: error.validation },
            "Validation failed",
          );
      
          return reply.status(HTTP_BAD_REQUEST).send({
            statusCode: HTTP_BAD_REQUEST,
            error: "Bad Request",
            message: "Validation failed",
            details: error.validation,
          });
        }
      
        // Custom application errors
        if ("statusCode" in error && typeof error.statusCode === "number") {
          request.log.warn(
            { reqId: request.id, error: error.message },
            "Application error",
          );
      
          return reply.status(error.statusCode).send({
            statusCode: error.statusCode,
            error: error.name,
            message: error.message,
            ...("details" in error ? { details: error.details } : {}),
          });
        }
      
        // Unexpected errors - log full details but send generic response
        request.log.error(
          { reqId: request.id, error: error.message, stack: error.stack },
          "Unexpected error",
        );
      
        return reply.status(HTTP_INTERNAL_ERROR).send({
          statusCode: HTTP_INTERNAL_ERROR,
          error: "Internal Server Error",
          message: "An unexpected error occurred",
        });
      };
      ```
      
      **Why good:** Fastify validation errors handled specially (expose details), custom errors with statusCode, unexpected errors logged with stack but hidden from client
      
      ---
      
      ## Pattern 4: Service Injection via Decorators
      
      ### Good Example - Registration Order
      
      ```typescript
      // src/server.ts
      import Fastify from "fastify";
      import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
      
      import { appConfig } from "./plugins/config";
      import { services } from "./plugins/services";
      import { userRoutes } from "./routes/users";
      
      const buildServer = () => {
        const server = Fastify({
          logger: { level: process.env.LOG_LEVEL ?? "info" },
        }).withTypeProvider<TypeBoxTypeProvider>();
      
        // 1. Infrastructure plugins first (no dependencies)
        server.register(appConfig);
      
        // 2. Service plugins (may depend on infrastructure)
        server.register(services);
      
        // 3. Route plugins last (depend on all above)
        server.register(userRoutes, { prefix: "/api/users" });
      
        return server;
      };
      
      export { buildServer };
      ```
      
      **Why good:** Dependencies declared explicitly, clear registration order, infrastructure before services before routes
      
      ---
      
      ## Pattern 5: Testing with server.inject()
      
      ### Good Example - Test Structure
      
      ```typescript
      // src/server.test.ts
      import { describe, it, expect, beforeEach, afterEach } from "your-test-runner";
      
      import { buildServer } from "./server";
      
      describe("User API", () => {
        let server: ReturnType<typeof buildServer>;
      
        beforeEach(async () => {
          server = buildServer();
          await server.ready();
        });
      
        afterEach(async () => {
          await server.close();
        });
      
        it("should list users", async () => {
          const response = await server.inject({
            method: "GET",
            url: "/api/users",
            query: { limit: "10" },
          });
      
          expect(response.statusCode).toBe(200);
      
          const body = response.json();
          expect(body).toHaveProperty("users");
          expect(body).toHaveProperty("total");
        });
      
        it("should validate query parameters", async () => {
          const response = await server.inject({
            method: "GET",
            url: "/api/users",
            query: { limit: "-1" }, // Invalid: below minimum
          });
      
          expect(response.statusCode).toBe(400);
      
          const body = response.json();
          expect(body.message).toContain("Validation failed");
        });
      
        it("should create user with valid data", async () => {
          const response = await server.inject({
            method: "POST",
            url: "/api/users",
            payload: {
              username: "testuser",
              email: "test@example.com",
            },
          });
      
          expect(response.statusCode).toBe(201);
      
          const body = response.json();
          expect(body.username).toBe("testuser");
          expect(body.id).toBeDefined();
        });
      
        it("should reject invalid email format", async () => {
          const response = await server.inject({
            method: "POST",
            url: "/api/users",
            payload: {
              username: "testuser",
              email: "not-an-email",
            },
          });
      
          expect(response.statusCode).toBe(400);
        });
      });
      ```
      
      **Why good:** `server.inject()` tests without network overhead, `beforeEach`/`afterEach` ensures clean state, tests validation and success paths, no external test runner dependency
      
    • hooks.md 12.1 KB
      # Fastify - Lifecycle Hooks Examples
      
      > Request lifecycle, hooks, and cross-cutting concerns. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Prerequisites**: Understand [Pattern 5: Lifecycle Hooks](../SKILL.md) from core patterns.
      
      ---
      
      ## Hook Execution Order
      
      Fastify hooks execute in this order:
      
      1. `onRequest` - Before parsing (first hook)
      2. `preParsing` - Before body parsing
      3. `preValidation` - Before schema validation
      4. `preHandler` - After validation, before handler
      5. `preSerialization` - Before response serialization
      6. `onSend` - Before sending response
      7. `onResponse` - After response sent (async, non-blocking)
      8. `onError` - When error occurs (can modify response)
      
      ---
      
      ## onRequest Hook
      
      ### Good Example - Request Timing and ID
      
      ```typescript
      // src/plugins/request-timing.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      import { randomUUID } from "node:crypto";
      
      const REQUEST_ID_HEADER = "x-request-id";
      
      declare module "fastify" {
        interface FastifyRequest {
          requestId: string;
          startTime: bigint;
        }
      }
      
      const requestTimingPlugin: FastifyPluginAsync = async (fastify) => {
        fastify.decorateRequest("requestId", "");
        fastify.decorateRequest("startTime", BigInt(0));
      
        // onRequest: First hook, before any parsing
        fastify.addHook("onRequest", async (request) => {
          request.requestId =
            request.headers[REQUEST_ID_HEADER]?.toString() ?? randomUUID();
          request.startTime = process.hrtime.bigint();
      
          request.log.info({ requestId: request.requestId }, "Request started");
        });
      };
      
      export const requestTiming = fp(requestTimingPlugin, {
        name: "request-timing",
      });
      ```
      
      **Why good:** Captures timing before any processing, generates request ID if not provided, logs request start
      
      ---
      
      ## preHandler Hook
      
      ### Good Example - Authentication
      
      ```typescript
      // src/plugins/auth.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from "fastify";
      
      const HTTP_UNAUTHORIZED = 401;
      const BEARER_PREFIX = "Bearer ";
      
      declare module "fastify" {
        interface FastifyRequest {
          userId: string | null;
          userRole: string | null;
        }
      }
      
      // Route-level hook for authentication
      export const requireAuth = async (
        request: FastifyRequest,
        reply: FastifyReply,
      ) => {
        const authHeader = request.headers.authorization;
      
        if (!authHeader || !authHeader.startsWith(BEARER_PREFIX)) {
          return reply.status(HTTP_UNAUTHORIZED).send({
            statusCode: HTTP_UNAUTHORIZED,
            error: "Unauthorized",
            message: "Missing or invalid authorization header",
          });
        }
      
        const token = authHeader.slice(BEARER_PREFIX.length);
      
        try {
          const decoded = await request.server.authService.verifyToken(token);
          request.userId = decoded.userId;
          request.userRole = decoded.role;
        } catch {
          return reply.status(HTTP_UNAUTHORIZED).send({
            statusCode: HTTP_UNAUTHORIZED,
            error: "Unauthorized",
            message: "Invalid or expired token",
          });
        }
      };
      
      // Plugin to set up request decorators
      const authPlugin: FastifyPluginAsync = async (fastify) => {
        fastify.decorateRequest("userId", null);
        fastify.decorateRequest("userRole", null);
      };
      
      export const auth = fp(authPlugin, {
        name: "auth",
      });
      ```
      
      **Usage in routes:**
      
      ```typescript
      // Apply to specific routes
      fastify.get(
        "/profile",
        { preHandler: [requireAuth] },
        async (request, reply) => {
          // request.userId is set by auth hook
          const user = await fastify.userService.findById(request.userId!);
          return user;
        },
      );
      
      // Apply to all routes in a plugin
      export const protectedRoutes: FastifyPluginAsync = async (fastify) => {
        fastify.addHook("preHandler", requireAuth);
      
        fastify.get("/dashboard", async (request) => {
          // All routes here require auth
        });
      };
      ```
      
      **Why good:** preHandler runs after validation (body is parsed), can short-circuit with reply, sets request decorators for downstream use
      
      ---
      
      ## onResponse Hook
      
      ### Good Example - Request Logging and Metrics
      
      ```typescript
      // src/plugins/request-logger.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      
      const requestLoggerPlugin: FastifyPluginAsync = async (fastify) => {
        // onResponse: After response sent (non-blocking)
        fastify.addHook("onResponse", async (request, reply) => {
          const duration = process.hrtime.bigint() - request.startTime;
          const durationMs = Number(duration) / 1e6;
      
          request.log.info(
            {
              requestId: request.requestId,
              method: request.method,
              url: request.url,
              statusCode: reply.statusCode,
              durationMs: durationMs.toFixed(2),
              contentLength: reply.getHeader("content-length"),
            },
            "Request completed",
          );
      
          // Send to metrics system
          if (fastify.metrics) {
            fastify.metrics.recordRequest({
              method: request.method,
              path: request.routeOptions.url,
              statusCode: reply.statusCode,
              duration: durationMs,
            });
          }
        });
      };
      
      export const requestLogger = fp(requestLoggerPlugin, {
        name: "request-logger",
        dependencies: ["request-timing"],
      });
      ```
      
      **Why good:** onResponse is async and non-blocking (doesn't delay response), captures final status code and timing, good for metrics
      
      ---
      
      ## onError Hook
      
      ### Good Example - Error Logging and Transformation
      
      ```typescript
      // src/plugins/error-logger.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      
      interface AppError extends Error {
        statusCode?: number;
        code?: string;
      }
      
      const errorLoggerPlugin: FastifyPluginAsync = async (fastify) => {
        // onError: When error occurs in handler
        fastify.addHook("onError", async (request, reply, error: AppError) => {
          const isClientError =
            error.statusCode && error.statusCode >= 400 && error.statusCode < 500;
      
          if (isClientError) {
            // Log client errors as warnings
            request.log.warn(
              {
                requestId: request.requestId,
                error: error.message,
                code: error.code,
                statusCode: error.statusCode,
              },
              "Client error",
            );
          } else {
            // Log server errors with full stack
            request.log.error(
              {
                requestId: request.requestId,
                error: error.message,
                code: error.code,
                stack: error.stack,
                statusCode: error.statusCode || 500,
              },
              "Server error",
            );
      
            // Report to error tracking service
            if (fastify.errorTracker) {
              fastify.errorTracker.capture(error, {
                requestId: request.requestId,
                userId: request.userId,
                url: request.url,
              });
            }
          }
        });
      };
      
      export const errorLogger = fp(errorLoggerPlugin, {
        name: "error-logger",
        dependencies: ["request-timing"],
      });
      ```
      
      **Why good:** Distinguishes client vs server errors for logging levels, captures full context, integrates with error tracking
      
      ---
      
      ## preSerialization Hook
      
      ### Good Example - Response Transformation
      
      ```typescript
      // src/plugins/response-wrapper.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      
      const HTTP_SUCCESS_MIN = 200;
      const HTTP_SUCCESS_MAX = 299;
      
      interface WrappedResponse {
        success: boolean;
        data: unknown;
        meta?: {
          requestId: string;
          timestamp: string;
        };
      }
      
      const responseWrapperPlugin: FastifyPluginAsync = async (fastify) => {
        // preSerialization: Transform payload before stringify
        fastify.addHook("preSerialization", async (request, reply, payload) => {
          // Skip if already wrapped or error response
          if (payload && typeof payload === "object" && "success" in payload) {
            return payload;
          }
      
          // Only wrap successful responses
          const isSuccess =
            reply.statusCode >= HTTP_SUCCESS_MIN &&
            reply.statusCode <= HTTP_SUCCESS_MAX;
      
          if (!isSuccess) {
            return payload;
          }
      
          const wrapped: WrappedResponse = {
            success: true,
            data: payload,
            meta: {
              requestId: request.requestId,
              timestamp: new Date().toISOString(),
            },
          };
      
          return wrapped;
        });
      };
      
      export const responseWrapper = fp(responseWrapperPlugin, {
        name: "response-wrapper",
        dependencies: ["request-timing"],
      });
      ```
      
      **Why good:** preSerialization transforms before JSON.stringify, conditionally wraps only success responses, adds metadata
      
      ---
      
      ## onSend Hook
      
      ### Good Example - Response Headers
      
      ```typescript
      // src/plugins/response-headers.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      
      const responseHeadersPlugin: FastifyPluginAsync = async (fastify) => {
        // onSend: Last chance to modify response
        fastify.addHook("onSend", async (request, reply, payload) => {
          // Add request ID to response headers
          reply.header("x-request-id", request.requestId);
      
          // Add cache headers for GET requests
          if (request.method === "GET" && !reply.hasHeader("cache-control")) {
            reply.header("cache-control", "no-store");
          }
      
          // Calculate and add duration header
          if (request.startTime) {
            const duration = process.hrtime.bigint() - request.startTime;
            const durationMs = Number(duration) / 1e6;
            reply.header("x-response-time", `${durationMs.toFixed(2)}ms`);
          }
      
          return payload;
        });
      };
      
      export const responseHeaders = fp(responseHeadersPlugin, {
        name: "response-headers",
        dependencies: ["request-timing"],
      });
      ```
      
      **Why good:** onSend is last chance before response, adds correlation headers, calculates response time
      
      ---
      
      ## Route-Level vs Plugin-Level Hooks
      
      ### Good Example - Selective Hook Application
      
      ```typescript
      // src/routes/admin.ts
      import type { FastifyPluginAsync } from "fastify";
      import { requireAuth } from "../plugins/auth";
      
      const HTTP_FORBIDDEN = 403;
      
      // Admin authorization hook
      const requireAdmin = async (request: FastifyRequest, reply: FastifyReply) => {
        if (request.userRole !== "admin") {
          return reply.status(HTTP_FORBIDDEN).send({
            statusCode: HTTP_FORBIDDEN,
            error: "Forbidden",
            message: "Admin access required",
          });
        }
      };
      
      export const adminRoutes: FastifyPluginAsync = async (fastify) => {
        // Plugin-level: applies to ALL routes in this plugin
        fastify.addHook("preHandler", requireAuth);
        fastify.addHook("preHandler", requireAdmin);
      
        // All routes below require admin auth
        fastify.get("/stats", async () => {
          return fastify.statsService.getAll();
        });
      
        fastify.get("/users", async () => {
          return fastify.userService.listAll();
        });
      };
      
      // Alternative: Route-level hooks
      export const mixedRoutes: FastifyPluginAsync = async (fastify) => {
        // Public route - no hooks
        fastify.get("/public", async () => {
          return { message: "Public content" };
        });
      
        // Protected route - route-level preHandler
        fastify.get("/protected", { preHandler: [requireAuth] }, async (request) => {
          return { userId: request.userId };
        });
      
        // Admin route - multiple route-level hooks
        fastify.delete(
          "/users/:id",
          { preHandler: [requireAuth, requireAdmin] },
          async (request) => {
            // Only admins
          },
        );
      };
      ```
      
      **Why good:** Plugin-level for consistent protection, route-level for selective application, hooks execute in array order
      
      ---
      
      ## Quick Reference
      
      | Hook               | Timing            | Use For                       |
      | ------------------ | ----------------- | ----------------------------- |
      | `onRequest`        | Before parsing    | Request ID, timing start      |
      | `preParsing`       | Before body parse | Stream transformation         |
      | `preValidation`    | Before validation | Custom validation             |
      | `preHandler`       | After validation  | Auth, authorization           |
      | `preSerialization` | Before stringify  | Response transformation       |
      | `onSend`           | Before send       | Headers, final modifications  |
      | `onResponse`       | After sent        | Logging, metrics              |
      | `onError`          | On error          | Error logging, transformation |
      
      | Hook Level                            | Applies To                |
      | ------------------------------------- | ------------------------- |
      | `fastify.addHook()` in plugin         | All routes in that plugin |
      | Route options `{ preHandler: [...] }` | Single route              |
      | Root-level after fp() plugin          | All routes                |
      
    • plugins.md 11.1 KB
      # Fastify - Plugin Examples
      
      > Plugin system, encapsulation, and decorators. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Prerequisites**: Understand [Pattern 4: Plugin Encapsulation](../SKILL.md) from core patterns.
      
      ---
      
      ## Plugin Basics
      
      ### Good Example - Encapsulated Domain Plugin
      
      ```typescript
      // src/plugins/auth-routes.ts
      import type { FastifyPluginAsync } from "fastify";
      import { Type } from "@fastify/type-provider-typebox";
      
      const HTTP_OK = 200;
      const HTTP_CREATED = 201;
      const HTTP_UNAUTHORIZED = 401;
      
      // This plugin is ENCAPSULATED - decorators stay within this scope
      export const authRoutes: FastifyPluginAsync = async (fastify) => {
        // Local decorator - only available in this plugin
        fastify.decorate("authConfig", {
          tokenExpiry: 3600,
          refreshExpiry: 86400,
        });
      
        fastify.post(
          "/login",
          {
            schema: {
              body: Type.Object({
                email: Type.String({ format: "email" }),
                password: Type.String({ minLength: 8 }),
              }),
              response: {
                [HTTP_OK]: Type.Object({
                  token: Type.String(),
                  expiresIn: Type.Integer(),
                }),
                [HTTP_UNAUTHORIZED]: Type.Object({
                  message: Type.String(),
                }),
              },
            },
          },
          async (request, reply) => {
            const { email, password } = request.body;
      
            const user = await validateCredentials(email, password);
            if (!user) {
              return reply.status(HTTP_UNAUTHORIZED).send({
                message: "Invalid credentials",
              });
            }
      
            const token = generateToken(user, fastify.authConfig.tokenExpiry);
      
            return reply.status(HTTP_OK).send({
              token,
              expiresIn: fastify.authConfig.tokenExpiry,
            });
          },
        );
      
        fastify.post("/logout", async (request, reply) => {
          // Logout logic
          return reply.status(HTTP_OK).send({ success: true });
        });
      };
      ```
      
      **Why good:** Plugin is encapsulated, authConfig decorator only accessible within this plugin, schemas define request/response types
      
      ---
      
      ## Shared Plugins with fastify-plugin
      
      ### Good Example - Database Connection Plugin
      
      ```typescript
      // src/plugins/database.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      
      // Type augmentation for shared decorator
      declare module "fastify" {
        interface FastifyInstance {
          db: {
            query: <T>(sql: string, params?: unknown[]) => Promise<T[]>;
            close: () => Promise<void>;
          };
        }
      }
      
      const databasePlugin: FastifyPluginAsync = async (fastify) => {
        // Initialize database connection
        const pool = createDatabasePool({
          host: process.env.DB_HOST,
          port: Number(process.env.DB_PORT),
          database: process.env.DB_NAME,
        });
      
        const db = {
          query: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {
            const result = await pool.query(sql, params);
            return result.rows as T[];
          },
          close: async () => {
            await pool.end();
          },
        };
      
        // Expose to parent scope via fastify-plugin
        fastify.decorate("db", db);
      
        // Cleanup on server close
        fastify.addHook("onClose", async () => {
          await db.close();
          fastify.log.info("Database connection closed");
        });
      };
      
      // Wrap with fastify-plugin to break encapsulation
      export const database = fp(databasePlugin, {
        name: "database",
        dependencies: [], // No dependencies
      });
      ```
      
      **Why good:** fastify-plugin exposes decorator to parent scope, type augmentation provides TypeScript support, onClose hook for cleanup
      
      ### Bad Example - Shared Plugin Without fp
      
      ```typescript
      // WRONG - Decorator not visible to other plugins
      import type { FastifyPluginAsync } from "fastify";
      
      export const databasePlugin: FastifyPluginAsync = async (fastify) => {
        fastify.decorate("db", createDatabase());
        // This decorator is encapsulated!
      };
      
      // In another plugin:
      fastify.register(databasePlugin);
      fastify.register(async (f) => {
        f.db; // undefined! Encapsulation blocks access
      });
      ```
      
      **Why bad:** Without fastify-plugin wrapper, decorators are encapsulated and invisible to sibling plugins
      
      ---
      
      ## Plugin Registration Order
      
      ### Good Example - Dependency Management
      
      ```typescript
      // src/server.ts
      import Fastify from "fastify";
      import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
      
      import { database } from "./plugins/database";
      import { auth } from "./plugins/auth";
      import { requestContext } from "./plugins/request-context";
      import { userRoutes } from "./routes/users";
      import { productRoutes } from "./routes/products";
      
      const SERVER_PORT = 3000;
      const SERVER_HOST = "0.0.0.0";
      
      const buildServer = () => {
        const server = Fastify({
          logger: { level: process.env.LOG_LEVEL ?? "info" },
        }).withTypeProvider<TypeBoxTypeProvider>();
      
        // 1. Infrastructure plugins first (no dependencies)
        server.register(database);
        server.register(requestContext);
      
        // 2. Middleware plugins (may depend on infrastructure)
        server.register(auth);
      
        // 3. Route plugins last (depend on all above)
        server.register(userRoutes, { prefix: "/api/users" });
        server.register(productRoutes, { prefix: "/api/products" });
      
        return server;
      };
      
      export { buildServer };
      ```
      
      **Why good:** Clear registration order, infrastructure before middleware before routes, prefix option for route namespacing
      
      ---
      
      ## Plugin with Dependencies
      
      ### Good Example - Declaring Dependencies
      
      ```typescript
      // src/plugins/user-service.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      
      interface UserService {
        findById: (id: string) => Promise<User | null>;
        create: (data: CreateUserInput) => Promise<User>;
        update: (id: string, data: UpdateUserInput) => Promise<User>;
      }
      
      declare module "fastify" {
        interface FastifyInstance {
          userService: UserService;
        }
      }
      
      const userServicePlugin: FastifyPluginAsync = async (fastify) => {
        // Access db decorator from database plugin
        const { db } = fastify;
      
        const userService: UserService = {
          findById: async (id) => {
            const [user] = await db.query<User>("SELECT * FROM users WHERE id = $1", [
              id,
            ]);
            return user || null;
          },
      
          create: async (data) => {
            const [user] = await db.query<User>(
              "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
              [data.name, data.email],
            );
            return user;
          },
      
          update: async (id, data) => {
            const [user] = await db.query<User>(
              "UPDATE users SET name = $1 WHERE id = $2 RETURNING *",
              [data.name, id],
            );
            return user;
          },
        };
      
        fastify.decorate("userService", userService);
      };
      
      export const userService = fp(userServicePlugin, {
        name: "user-service",
        dependencies: ["database"], // Requires database plugin
      });
      ```
      
      **Why good:** dependencies array ensures registration order, TypeScript augmentation for type safety, uses db decorator from database plugin
      
      ---
      
      ## Request Decorators
      
      ### Good Example - Per-Request State
      
      ```typescript
      // src/plugins/request-context.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync } from "fastify";
      import { randomUUID } from "node:crypto";
      
      const REQUEST_ID_HEADER = "x-request-id";
      
      declare module "fastify" {
        interface FastifyRequest {
          requestId: string;
          startTime: bigint;
          userId: string | null;
        }
      }
      
      const requestContextPlugin: FastifyPluginAsync = async (fastify) => {
        // Decorate with initial values
        fastify.decorateRequest("requestId", "");
        fastify.decorateRequest("startTime", BigInt(0));
        fastify.decorateRequest("userId", null);
      
        // Set values per-request
        fastify.addHook("onRequest", async (request) => {
          request.requestId =
            request.headers[REQUEST_ID_HEADER]?.toString() ?? randomUUID();
          request.startTime = process.hrtime.bigint();
        });
      };
      
      export const requestContext = fp(requestContextPlugin, {
        name: "request-context",
      });
      ```
      
      **Usage in routes:**
      
      ```typescript
      fastify.get("/", async (request, reply) => {
        // Access request decorators
        request.log.info({ requestId: request.requestId }, "Processing request");
        return { requestId: request.requestId };
      });
      ```
      
      **Why good:** decorateRequest with initial values, onRequest hook sets per-request state, type-safe access in routes
      
      ---
      
      ## Reply Decorators
      
      ### Good Example - Response Helpers
      
      ```typescript
      // src/plugins/reply-helpers.ts
      import fp from "fastify-plugin";
      import type { FastifyPluginAsync, FastifyReply } from "fastify";
      
      const HTTP_BAD_REQUEST = 400;
      const HTTP_NOT_FOUND = 404;
      const HTTP_CONFLICT = 409;
      
      declare module "fastify" {
        interface FastifyReply {
          notFound: (message: string) => void;
          badRequest: (message: string, details?: unknown) => void;
          conflict: (message: string) => void;
        }
      }
      
      const replyHelpersPlugin: FastifyPluginAsync = async (fastify) => {
        fastify.decorateReply(
          "notFound",
          function (this: FastifyReply, message: string) {
            this.status(HTTP_NOT_FOUND).send({
              statusCode: HTTP_NOT_FOUND,
              error: "Not Found",
              message,
            });
          },
        );
      
        fastify.decorateReply(
          "badRequest",
          function (this: FastifyReply, message: string, details?: unknown) {
            this.status(HTTP_BAD_REQUEST).send({
              statusCode: HTTP_BAD_REQUEST,
              error: "Bad Request",
              message,
              ...(details ? { details } : {}),
            });
          },
        );
      
        fastify.decorateReply(
          "conflict",
          function (this: FastifyReply, message: string) {
            this.status(HTTP_CONFLICT).send({
              statusCode: HTTP_CONFLICT,
              error: "Conflict",
              message,
            });
          },
        );
      };
      
      export const replyHelpers = fp(replyHelpersPlugin, {
        name: "reply-helpers",
      });
      ```
      
      **Usage:**
      
      ```typescript
      fastify.get("/:id", async (request, reply) => {
        const user = await fastify.userService.findById(request.params.id);
      
        if (!user) {
          return reply.notFound(`User ${request.params.id} not found`);
        }
      
        return user;
      });
      
      fastify.post("/", async (request, reply) => {
        const exists = await fastify.userService.findByEmail(request.body.email);
      
        if (exists) {
          return reply.conflict("Email already registered");
        }
      
        return fastify.userService.create(request.body);
      });
      ```
      
      **Why good:** Reply helpers reduce boilerplate, consistent error format, `this` binding for access to reply instance
      
      ---
      
      ## Quick Reference
      
      | Plugin Type           | Wrapper | Decorator Visibility      |
      | --------------------- | ------- | ------------------------- |
      | Domain/route plugin   | None    | Encapsulated (local only) |
      | Shared infrastructure | `fp()`  | Exposed to parent scope   |
      | Utility plugin        | `fp()`  | Exposed to parent scope   |
      
      | fp() Option    | Purpose                             |
      | -------------- | ----------------------------------- |
      | `name`         | Plugin identifier for debugging     |
      | `dependencies` | Required plugins (registered first) |
      | `fastify`      | Fastify version constraint          |
      | `encapsulate`  | Override encapsulation behavior     |
      
      | Decorator Type      | Initial Value        | Set Per-Request     |
      | ------------------- | -------------------- | ------------------- |
      | `decorate()`        | Any                  | No (instance-level) |
      | `decorateRequest()` | Required             | Yes (in hooks)      |
      | `decorateReply()`   | Function with `this` | No (method)         |
      
    • schemas.md 12.4 KB
      # Fastify - Schema Examples
      
      > TypeBox schemas, validation, and type-safe routes. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Prerequisites**: Understand [Pattern 2: Schema Definition](../SKILL.md) from core patterns.
      
      ---
      
      ## Pattern 3: Complete Schema Set
      
      ### Good Example - Full Domain Schemas
      
      ```typescript
      // src/schemas/user.ts
      import { Type, Static } from "@fastify/type-provider-typebox";
      
      // Named constants for validation constraints
      const MIN_USERNAME_LENGTH = 3;
      const MAX_USERNAME_LENGTH = 50;
      const MIN_PASSWORD_LENGTH = 8;
      const MIN_AGE = 0;
      const MAX_AGE = 150;
      
      // Base user schema
      export const UserSchema = Type.Object({
        id: Type.String({ format: "uuid" }),
        username: Type.String({
          minLength: MIN_USERNAME_LENGTH,
          maxLength: MAX_USERNAME_LENGTH,
        }),
        email: Type.String({ format: "email" }),
        age: Type.Optional(Type.Integer({ minimum: MIN_AGE, maximum: MAX_AGE })),
        role: Type.Union([
          Type.Literal("user"),
          Type.Literal("admin"),
          Type.Literal("moderator"),
        ]),
        createdAt: Type.String({ format: "date-time" }),
        updatedAt: Type.String({ format: "date-time" }),
      });
      
      // Create input (subset of User)
      export const CreateUserSchema = Type.Object({
        username: Type.String({
          minLength: MIN_USERNAME_LENGTH,
          maxLength: MAX_USERNAME_LENGTH,
        }),
        email: Type.String({ format: "email" }),
        password: Type.String({ minLength: MIN_PASSWORD_LENGTH }),
        age: Type.Optional(Type.Integer({ minimum: MIN_AGE, maximum: MAX_AGE })),
      });
      
      // Update input (all optional)
      export const UpdateUserSchema = Type.Partial(
        Type.Pick(CreateUserSchema, ["username", "email", "age"]),
      );
      
      // Route params
      export const UserParamsSchema = Type.Object({
        id: Type.String({ format: "uuid" }),
      });
      
      // Query params for list
      export const UsersQuerySchema = Type.Object({
        page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),
        limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),
        sort: Type.Optional(
          Type.Union([
            Type.Literal("username"),
            Type.Literal("email"),
            Type.Literal("createdAt"),
          ]),
        ),
        order: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")])),
      });
      
      // Derive TypeScript types
      export type User = Static<typeof UserSchema>;
      export type CreateUser = Static<typeof CreateUserSchema>;
      export type UpdateUser = Static<typeof UpdateUserSchema>;
      export type UserParams = Static<typeof UserParamsSchema>;
      export type UsersQuery = Static<typeof UsersQuerySchema>;
      ```
      
      **Why good:** Named constants for constraints, Static<> derives types from schemas, Type.Partial for update schemas, Type.Union for enums
      
      ### Bad Example - Separate Types and Validation (DRIFT RISK)
      
      ```typescript
      // WRONG - Types and validation can drift
      interface User {
        id: string;
        username: string;
        email: string;
      }
      
      const validateUser = (data: unknown): data is User => {
        // Manual validation...
        return true;
      };
      
      // Later someone adds a field to interface but not validation
      interface User {
        id: string;
        username: string;
        email: string;
        role: string; // Added here, not in validation!
      }
      ```
      
      **Why bad:** Manual validation drifts from types, no JSON Schema for documentation, no Fastify integration
      
      ---
      
      ## Pattern 4: Route with Full Schema
      
      ### Good Example - Complete CRUD Routes
      
      ```typescript
      // src/routes/users.ts
      import type { FastifyPluginAsync } from "fastify";
      import { Type } from "@fastify/type-provider-typebox";
      
      import {
        UserSchema,
        CreateUserSchema,
        UpdateUserSchema,
        UserParamsSchema,
        UsersQuerySchema,
      } from "../schemas/user";
      
      const HTTP_OK = 200;
      const HTTP_CREATED = 201;
      const HTTP_NO_CONTENT = 204;
      const HTTP_NOT_FOUND = 404;
      
      const ErrorSchema = Type.Object({
        statusCode: Type.Integer(),
        error: Type.String(),
        message: Type.String(),
      });
      
      export const userRoutes: FastifyPluginAsync = async (fastify) => {
        // GET /api/users
        fastify.get(
          "/",
          {
            schema: {
              querystring: UsersQuerySchema,
              response: {
                [HTTP_OK]: Type.Object({
                  data: Type.Array(UserSchema),
                  pagination: Type.Object({
                    page: Type.Integer(),
                    limit: Type.Integer(),
                    total: Type.Integer(),
                    totalPages: Type.Integer(),
                  }),
                }),
              },
            },
          },
          async (request, reply) => {
            // request.query is fully typed as UsersQuery
            const { page = 1, limit = 20, sort, order } = request.query;
      
            const offset = (page - 1) * limit;
            const [users, total] = await Promise.all([
              fastify.userService.list({ offset, limit, sort, order }),
              fastify.userService.count(),
            ]);
      
            return reply.status(HTTP_OK).send({
              data: users,
              pagination: {
                page,
                limit,
                total,
                totalPages: Math.ceil(total / limit),
              },
            });
          },
        );
      
        // GET /api/users/:id
        fastify.get(
          "/:id",
          {
            schema: {
              params: UserParamsSchema,
              response: {
                [HTTP_OK]: UserSchema,
                [HTTP_NOT_FOUND]: ErrorSchema,
              },
            },
          },
          async (request, reply) => {
            // request.params.id is typed as string (uuid format)
            const user = await fastify.userService.findById(request.params.id);
      
            if (!user) {
              return reply.status(HTTP_NOT_FOUND).send({
                statusCode: HTTP_NOT_FOUND,
                error: "Not Found",
                message: `User ${request.params.id} not found`,
              });
            }
      
            return reply.status(HTTP_OK).send(user);
          },
        );
      
        // POST /api/users
        fastify.post(
          "/",
          {
            schema: {
              body: CreateUserSchema,
              response: {
                [HTTP_CREATED]: UserSchema,
              },
            },
          },
          async (request, reply) => {
            // request.body is typed as CreateUser
            const user = await fastify.userService.create(request.body);
            return reply.status(HTTP_CREATED).send(user);
          },
        );
      
        // PATCH /api/users/:id
        fastify.patch(
          "/:id",
          {
            schema: {
              params: UserParamsSchema,
              body: UpdateUserSchema,
              response: {
                [HTTP_OK]: UserSchema,
                [HTTP_NOT_FOUND]: ErrorSchema,
              },
            },
          },
          async (request, reply) => {
            const user = await fastify.userService.update(
              request.params.id,
              request.body,
            );
      
            if (!user) {
              return reply.status(HTTP_NOT_FOUND).send({
                statusCode: HTTP_NOT_FOUND,
                error: "Not Found",
                message: `User ${request.params.id} not found`,
              });
            }
      
            return reply.status(HTTP_OK).send(user);
          },
        );
      
        // DELETE /api/users/:id
        fastify.delete(
          "/:id",
          {
            schema: {
              params: UserParamsSchema,
              response: {
                [HTTP_NO_CONTENT]: Type.Null(),
                [HTTP_NOT_FOUND]: ErrorSchema,
              },
            },
          },
          async (request, reply) => {
            const deleted = await fastify.userService.delete(request.params.id);
      
            if (!deleted) {
              return reply.status(HTTP_NOT_FOUND).send({
                statusCode: HTTP_NOT_FOUND,
                error: "Not Found",
                message: `User ${request.params.id} not found`,
              });
            }
      
            return reply.status(HTTP_NO_CONTENT).send();
          },
        );
      };
      ```
      
      **Why good:** Full schema for request (params, querystring, body) AND response, enables fast-json-stringify optimization, complete type inference
      
      ---
      
      ## Pattern 5: Reusable Schema Components
      
      ### Good Example - Shared Error and Pagination Schemas
      
      ```typescript
      // src/schemas/common.ts
      import { Type, TObject, TProperties } from "@fastify/type-provider-typebox";
      
      // Standard error response
      export const ErrorSchema = Type.Object({
        statusCode: Type.Integer(),
        error: Type.String(),
        message: Type.String(),
        details: Type.Optional(Type.Unknown()),
      });
      
      // Pagination wrapper factory
      export const PaginatedResponse = <T extends TProperties>(
        itemSchema: TObject<T>,
      ) =>
        Type.Object({
          data: Type.Array(itemSchema),
          pagination: Type.Object({
            page: Type.Integer(),
            limit: Type.Integer(),
            total: Type.Integer(),
            totalPages: Type.Integer(),
          }),
        });
      
      // Standard query params for paginated endpoints
      export const PaginationQuerySchema = Type.Object({
        page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),
        limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),
      });
      
      // ID param schema (reusable)
      export const IdParamSchema = Type.Object({
        id: Type.String({ format: "uuid" }),
      });
      ```
      
      **Usage:**
      
      ```typescript
      import {
        ErrorSchema,
        PaginatedResponse,
        IdParamSchema,
      } from "../schemas/common";
      import { UserSchema } from "../schemas/user";
      
      // Response schema
      const UsersListResponse = PaginatedResponse(UserSchema);
      
      fastify.get(
        "/",
        {
          schema: {
            response: {
              [HTTP_OK]: UsersListResponse,
            },
          },
        },
        async (request, reply) => {
          // ...
        },
      );
      ```
      
      **Why good:** Reusable schema components, factory function for paginated responses, consistent structure across endpoints
      
      ---
      
      ## Pattern 6: Schema Composition
      
      ### Good Example - Extending Schemas
      
      ```typescript
      // src/schemas/post.ts
      import { Type, Static } from "@fastify/type-provider-typebox";
      
      const MIN_TITLE_LENGTH = 1;
      const MAX_TITLE_LENGTH = 200;
      const MAX_CONTENT_LENGTH = 50000;
      
      // Base content fields
      const PostContentSchema = Type.Object({
        title: Type.String({
          minLength: MIN_TITLE_LENGTH,
          maxLength: MAX_TITLE_LENGTH,
        }),
        content: Type.String({ maxLength: MAX_CONTENT_LENGTH }),
        published: Type.Boolean({ default: false }),
        tags: Type.Optional(Type.Array(Type.String())),
      });
      
      // Timestamps (auto-generated)
      const TimestampSchema = Type.Object({
        createdAt: Type.String({ format: "date-time" }),
        updatedAt: Type.String({ format: "date-time" }),
      });
      
      // Full post with ID and timestamps
      export const PostSchema = Type.Intersect([
        Type.Object({ id: Type.String({ format: "uuid" }) }),
        PostContentSchema,
        TimestampSchema,
        Type.Object({
          authorId: Type.String({ format: "uuid" }),
        }),
      ]);
      
      // Create input (just content)
      export const CreatePostSchema = PostContentSchema;
      
      // Update input (partial content)
      export const UpdatePostSchema = Type.Partial(PostContentSchema);
      
      // Summary for list views
      export const PostSummarySchema = Type.Pick(PostSchema, [
        "id",
        "title",
        "published",
        "createdAt",
        "authorId",
      ]);
      
      // Types
      export type Post = Static<typeof PostSchema>;
      export type CreatePost = Static<typeof CreatePostSchema>;
      export type UpdatePost = Static<typeof UpdatePostSchema>;
      export type PostSummary = Static<typeof PostSummarySchema>;
      ```
      
      **Why good:** Type.Intersect for composition, Type.Pick for summary views, Type.Partial for update schemas, clear separation of concerns
      
      ---
      
      ## Validation Error Handling
      
      > See [core.md Pattern 3](core.md) for the full error handler with validation error mapping, custom error classes, and unexpected error handling.
      
      **Key point for schemas:** Fastify validation errors have a `.validation` array (not `.message`). Each entry contains `instancePath`, `message`, and `keyword` — use these to build client-friendly error details.
      
      ---
      
      ## Quick Reference
      
      | TypeBox Type            | JSON Schema                             | TypeScript        |
      | ----------------------- | --------------------------------------- | ----------------- |
      | `Type.String()`         | `{ type: "string" }`                    | `string`          |
      | `Type.Integer()`        | `{ type: "integer" }`                   | `number`          |
      | `Type.Boolean()`        | `{ type: "boolean" }`                   | `boolean`         |
      | `Type.Array(T)`         | `{ type: "array", items: T }`           | `T[]`             |
      | `Type.Object({...})`    | `{ type: "object", properties: {...} }` | `{ ... }`         |
      | `Type.Optional(T)`      | Property not required                   | `T \| undefined`  |
      | `Type.Union([...])`     | `{ anyOf: [...] }`                      | Union type        |
      | `Type.Literal("x")`     | `{ const: "x" }`                        | `"x"`             |
      | `Type.Partial(T)`       | All properties optional                 | `Partial<T>`      |
      | `Type.Pick(T, [...])`   | Subset of properties                    | `Pick<T, ...>`    |
      | `Type.Intersect([...])` | Merged schemas                          | Intersection type |
      
      | Schema Location  | Purpose                  |
      | ---------------- | ------------------------ |
      | `params`         | URL path parameters      |
      | `querystring`    | Query string parameters  |
      | `body`           | Request body             |
      | `headers`        | Request headers          |
      | `response[code]` | Response for status code |
      
  • reference.md 13.8 KB
    # Fastify Reference
    
    > Decision frameworks, anti-patterns, and red flags for Fastify development. Referenced from [SKILL.md](SKILL.md).
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### When to Use Fastify
    
    ```
    Need a Node.js web framework?
    ├─ Is performance critical (>10k req/sec)?
    │   ├─ YES → Fastify (2-3x faster than Express)
    │   └─ NO → Any framework works
    ├─ Need schema validation at framework level?
    │   ├─ YES → Fastify (built-in JSON Schema)
    │   └─ NO → Manual validation works
    ├─ Building large modular application?
    │   ├─ YES → Fastify (plugin encapsulation)
    │   └─ NO → Simpler patterns may suffice
    └─ Need Express middleware compatibility?
        ├─ YES → Consider Express or @fastify/express adapter
        └─ NO → Fastify native plugins preferred
    ```
    
    ### Type Provider Selection
    
    ```
    Which type provider?
    ├─ Want single source for types + validation?
    │   ├─ YES → TypeBox (@fastify/type-provider-typebox)
    │   └─ Already using Zod elsewhere?
    │       ├─ YES → fastify-type-provider-zod
    │       └─ NO → TypeBox (best integration)
    ├─ Have existing JSON Schemas?
    │   └─ YES → @fastify/type-provider-json-schema-to-ts
    └─ Default recommendation → TypeBox
    ```
    
    ### Plugin Encapsulation Decision
    
    ```
    Creating a new plugin?
    ├─ Is it shared infrastructure? (db, cache, logger)
    │   └─ YES → Use fastify-plugin (break encapsulation)
    ├─ Is it a domain/feature module?
    │   └─ YES → Keep encapsulated (default behavior)
    ├─ Does it add decorators other plugins need?
    │   └─ YES → Use fastify-plugin
    └─ Default → Keep encapsulated (safer)
    ```
    
    ### Hook Selection
    
    ```
    When should this code run?
    ├─ Before ANY processing (logging, request ID)?
    │   └─ onRequest
    ├─ Need to transform raw request stream?
    │   └─ preParsing
    ├─ Before schema validation?
    │   └─ preValidation
    ├─ After validation, before handler?
    │   └─ preHandler (authentication, authorization)
    ├─ Need to transform response object?
    │   └─ preSerialization
    ├─ Need to modify final payload string/buffer?
    │   └─ onSend (compression, encryption)
    ├─ After response sent (non-blocking)?
    │   └─ onResponse (metrics, logging)
    └─ On errors?
        └─ onError (custom error logging)
    ```
    
    ### Decorator vs Hook Decision
    
    ```
    Need to add functionality?
    ├─ Is it a utility/service available to all requests?
    │   └─ decorate() on FastifyInstance
    ├─ Is it per-request state?
    │   └─ decorateRequest() with hook to populate
    ├─ Is it response helper methods?
    │   └─ decorateReply()
    ├─ Is it cross-cutting behavior (auth, logging)?
    │   └─ addHook()
    └─ Is it one-time setup?
        └─ Run in plugin registration function
    ```
    
    ### Response Schema Decision
    
    ```
    Should I define a response schema?
    ├─ Is this a production API?
    │   └─ YES → Always define response schemas
    ├─ Is performance important?
    │   └─ YES → Response schemas enable fast-json-stringify
    ├─ Need to prevent accidental data leaks?
    │   └─ YES → Response schemas filter extra properties
    └─ Default → Define response schemas (best practice)
    ```
    
    </decision_framework>
    
    ---
    
    ## Red Flags
    
    > See [SKILL.md](SKILL.md) for the full red flags list.
    
    ---
    
    <anti_patterns>
    
    ## Anti-Patterns to Avoid
    
    ### Missing Type Provider
    
    ```typescript
    // WRONG: No type provider
    import Fastify from "fastify";
    
    const server = Fastify();
    
    server.get("/users", async (request) => {
      const limit = request.query.limit; // Type: unknown
      return { users: [] };
    });
    ```
    
    ```typescript
    // CORRECT: Type provider configured
    import Fastify from "fastify";
    import { Type, TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
    
    const server = Fastify().withTypeProvider<TypeBoxTypeProvider>();
    
    server.get(
      "/users",
      {
        schema: {
          querystring: Type.Object({
            limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
          }),
        },
      },
      async (request) => {
        const limit = request.query.limit; // Type: number | undefined
        return { users: [] };
      },
    );
    ```
    
    **Why it matters:** Without type provider, request.query/body/params are `unknown`, losing TypeScript benefits.
    
    ---
    
    ### Shared Plugin Without fastify-plugin
    
    ```typescript
    // WRONG: Decorator not accessible outside plugin
    import type { FastifyPluginAsync } from "fastify";
    
    export const databasePlugin: FastifyPluginAsync = async (fastify) => {
      fastify.decorate("db", createDatabaseClient());
    };
    
    // Usage - THIS FAILS:
    server.register(databasePlugin);
    server.register(async (fastify) => {
      const users = await fastify.db.query("..."); // db is undefined!
    });
    ```
    
    ```typescript
    // CORRECT: Use fastify-plugin for shared decorators
    import fp from "fastify-plugin";
    import type { FastifyPluginAsync } from "fastify";
    
    declare module "fastify" {
      interface FastifyInstance {
        db: DatabaseClient;
      }
    }
    
    const databasePlugin: FastifyPluginAsync = async (fastify) => {
      fastify.decorate("db", createDatabaseClient());
    };
    
    export const database = fp(databasePlugin, { name: "database" });
    
    // Usage - THIS WORKS:
    server.register(database);
    server.register(async (fastify) => {
      const users = await fastify.db.query("..."); // db is accessible!
    });
    ```
    
    **Why it matters:** Without fastify-plugin, decorators are encapsulated and invisible to sibling/parent contexts.
    
    ---
    
    ### Magic Numbers for Status Codes
    
    ```typescript
    // WRONG: Magic numbers scattered throughout code
    server.get("/:id", async (request, reply) => {
      const user = await findUser(request.params.id);
    
      if (!user) {
        return reply.status(404).send({ error: "Not found" });
      }
    
      return reply.status(200).send(user);
    });
    
    server.post("/", async (request, reply) => {
      try {
        const user = await createUser(request.body);
        return reply.status(201).send(user);
      } catch {
        return reply.status(500).send({ error: "Failed" });
      }
    });
    ```
    
    ```typescript
    // CORRECT: Named constants
    const HTTP_OK = 200;
    const HTTP_CREATED = 201;
    const HTTP_NOT_FOUND = 404;
    const HTTP_INTERNAL_ERROR = 500;
    
    server.get("/:id", async (request, reply) => {
      const user = await findUser(request.params.id);
    
      if (!user) {
        return reply.status(HTTP_NOT_FOUND).send({ error: "Not found" });
      }
    
      return reply.status(HTTP_OK).send(user);
    });
    
    server.post("/", async (request, reply) => {
      try {
        const user = await createUser(request.body);
        return reply.status(HTTP_CREATED).send(user);
      } catch {
        return reply.status(HTTP_INTERNAL_ERROR).send({ error: "Failed" });
      }
    });
    ```
    
    **Why it matters:** Named constants document intent, enable search/replace, prevent typos.
    
    ---
    
    ### Reference Types in Request Decorators
    
    ```typescript
    // WRONG: Shared mutable state across requests
    fastify.decorateRequest("userData", { name: "", permissions: [] });
    
    fastify.addHook("preHandler", async (request) => {
      request.userData.name = "John"; // MUTATES SHARED OBJECT!
      request.userData.permissions.push("read"); // ACCUMULATES ACROSS REQUESTS!
    });
    ```
    
    ```typescript
    // CORRECT: Initialize per-request in hook
    fastify.decorateRequest("userData", null);
    
    fastify.addHook("preHandler", async (request) => {
      request.userData = {
        name: "John",
        permissions: ["read"],
      };
    });
    ```
    
    **Why it matters:** Reference type decorators are shared across ALL requests - mutations persist and accumulate.
    
    ---
    
    ### Missing Response Schema
    
    ```typescript
    // WRONG: No response schema
    server.get("/users", async () => {
      const users = await db.query("SELECT * FROM users"); // May include password_hash!
      return { users };
    });
    ```
    
    ```typescript
    // CORRECT: Response schema filters and optimizes
    server.get(
      "/users",
      {
        schema: {
          response: {
            200: Type.Object({
              users: Type.Array(
                Type.Object({
                  id: Type.String(),
                  username: Type.String(),
                  email: Type.String(),
                  // password_hash NOT included - filtered out!
                }),
              ),
            }),
          },
        },
      },
      async () => {
        const users = await db.query("SELECT * FROM users");
        return { users }; // password_hash automatically removed
      },
    );
    ```
    
    **Why it matters:** Response schemas prevent accidental data leaks AND enable fast-json-stringify (2-3x faster).
    
    ---
    
    ### God Route Files
    
    ```typescript
    // WRONG: All routes in one file
    // routes.ts - 2000+ lines
    server.get("/users", async () => {});
    server.get("/users/:id", async () => {});
    server.post("/users", async () => {});
    server.get("/posts", async () => {});
    server.get("/posts/:id", async () => {});
    server.post("/posts", async () => {});
    // ... 50+ more routes
    ```
    
    ```typescript
    // CORRECT: Modular route plugins
    // server.ts
    server.register(userRoutes, { prefix: "/users" });
    server.register(postRoutes, { prefix: "/posts" });
    
    // routes/users.ts
    export const userRoutes: FastifyPluginAsync = async (fastify) => {
      fastify.get("/", listUsers);
      fastify.get("/:id", getUser);
      fastify.post("/", createUser);
    };
    
    // routes/posts.ts
    export const postRoutes: FastifyPluginAsync = async (fastify) => {
      fastify.get("/", listPosts);
      fastify.get("/:id", getPost);
      fastify.post("/", createPost);
    };
    ```
    
    **Why it matters:** Modular plugins enable encapsulation, easier testing, and maintainable code organization.
    
    ---
    
    ### Express Patterns in Fastify
    
    ```typescript
    // WRONG: Express-style patterns
    server.get("/users", (req, res, next) => {
      // res.send() doesn't exist
      // next() doesn't exist
      res.json({ users: [] });
    });
    
    server.use((req, res, next) => {
      // .use() works differently in Fastify
      next();
    });
    ```
    
    ```typescript
    // CORRECT: Fastify-native patterns
    server.get("/users", async (request, reply) => {
      return { users: [] }; // Auto-serialized to JSON
      // OR: reply.send({ users: [] });
    });
    
    server.addHook("onRequest", async (request, reply) => {
      // Hooks replace middleware
      // Return value or reply.send() short-circuits
    });
    ```
    
    **Why it matters:** Fastify uses different patterns - hooks instead of middleware, reply instead of res, return values for responses.
    
    ---
    
    ### Mixing Async and Callback Patterns
    
    ```typescript
    // WRONG: Mixing patterns causes confusion
    server.addHook("preHandler", async (request, reply, done) => {
      await someAsyncOperation();
      done(); // DON'T mix async with done callback
    });
    ```
    
    ```typescript
    // CORRECT: Use async consistently
    server.addHook("preHandler", async (request, reply) => {
      await someAsyncOperation();
      // No done() needed - async function completion signals done
    });
    
    // OR use callback consistently (rare)
    server.addHook("preHandler", (request, reply, done) => {
      someCallbackOperation((error) => {
        done(error);
      });
    });
    ```
    
    **Why it matters:** Mixing async/await with done callback can cause double-completion or hanging requests.
    
    </anti_patterns>
    
    ---
    
    ## Quick Reference Tables
    
    ### HTTP Status Constants
    
    ```typescript
    // Define once, use everywhere
    const HTTP_OK = 200;
    const HTTP_CREATED = 201;
    const HTTP_NO_CONTENT = 204;
    const HTTP_BAD_REQUEST = 400;
    const HTTP_UNAUTHORIZED = 401;
    const HTTP_FORBIDDEN = 403;
    const HTTP_NOT_FOUND = 404;
    const HTTP_CONFLICT = 409;
    const HTTP_UNPROCESSABLE_ENTITY = 422;
    const HTTP_TOO_MANY_REQUESTS = 429;
    const HTTP_INTERNAL_ERROR = 500;
    const HTTP_SERVICE_UNAVAILABLE = 503;
    ```
    
    ### Hook Execution Order
    
    | Hook             | When                     | Can Send Response     | Common Use                       |
    | ---------------- | ------------------------ | --------------------- | -------------------------------- |
    | onRequest        | First, before parsing    | Yes                   | Logging, request ID              |
    | preParsing       | Before body parse        | Yes                   | Stream transformation            |
    | preValidation    | Before schema validation | Yes                   | Modify body before validation    |
    | preHandler       | After validation         | Yes                   | Auth, authorization              |
    | handler          | Route handler            | Yes                   | Business logic                   |
    | preSerialization | Before JSON stringify    | No                    | Transform response object        |
    | onSend           | Before sending           | Yes (replace payload) | Compression, final modifications |
    | onResponse       | After sent               | No                    | Metrics, cleanup                 |
    | onError          | On error                 | No                    | Error logging                    |
    
    ### Type Provider Comparison
    
    | Provider                                 | Source          | Best For              |
    | ---------------------------------------- | --------------- | --------------------- |
    | @fastify/type-provider-typebox           | TypeBox schemas | New projects, best DX |
    | @fastify/type-provider-json-schema-to-ts | JSON Schema     | Existing JSON schemas |
    | fastify-type-provider-zod                | Zod schemas     | Already using Zod     |
    
    ---
    
    ## Production Checklist
    
    ### Before Deploying
    
    - [ ] Type provider configured with `withTypeProvider<>()`
    - [ ] All routes have schema definitions (querystring, params, body, response)
    - [ ] Response schemas defined for all endpoints
    - [ ] Named constants for HTTP status codes (no magic numbers)
    - [ ] Shared plugins wrapped with fastify-plugin
    - [ ] Plugin dependencies declared in options
    - [ ] Error handler configured with `setErrorHandler()`
    - [ ] Request ID generation and propagation
    - [ ] Proper logging configuration (production level)
    - [ ] onClose hooks for cleanup (database, connections)
    - [ ] No sensitive data in error responses
    - [ ] No PII in logs
    
    ### Testing Checklist
    
    - [ ] Server factory function for test isolation
    - [ ] Using `server.inject()` (no network overhead)
    - [ ] `beforeEach`: build server, `await server.ready()`
    - [ ] `afterEach`: `await server.close()`
    - [ ] Testing validation errors (400 responses)
    - [ ] Testing success paths
    - [ ] Testing error handling
    - [ ] Mocking decorators when needed
    
  • SKILL.md 13.9 KB
    ---
    name: api-framework-fastify
    description: Fastify routes, JSON Schema validation, plugin system, TypeScript type providers
    ---
    
    # API Development with Fastify
    
    > **Quick Guide:** Use Fastify for high-performance Node.js REST APIs with built-in JSON Schema validation and powerful plugin encapsulation. Use `@fastify/type-provider-typebox` for end-to-end type safety (both `Type` and `TypeBoxTypeProvider` re-exported from it). Wrap shared plugins with `fastify-plugin` to expose decorators. Always define response schemas for serialization performance and data leak prevention.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST use `withTypeProvider<>()` for type-safe request/response handling)**
    
    **(You MUST wrap shared plugins with `fastify-plugin` to expose decorators to parent scope)**
    
    **(You MUST define response schemas to enable fast-json-stringify optimization)**
    
    **(You MUST use named constants for HTTP status codes - never raw numbers)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Fastify, fastify.register, fastify.decorate, fastify-plugin, TypeBox, @fastify/type-provider-typebox, @fastify/type-provider-json-schema-to-ts, fastify-type-provider-zod, preHandler, onRequest, preSerialization, JSON Schema validation, fast-json-stringify, FastifyPluginAsyncTypebox
    
    **When to use:**
    
    - Building high-performance REST APIs (45k+ req/sec benchmarks)
    - Need schema-based validation with automatic coercion
    - Want plugin encapsulation for modular architecture
    - Require lifecycle hooks for cross-cutting concerns
    - Building APIs with strict TypeScript type safety requirements
    
    **When NOT to use:**
    
    - Simple internal APIs without performance requirements (consider your existing solution)
    - GraphQL APIs (use dedicated GraphQL servers)
    - Edge/serverless with size constraints (Fastify has larger footprint than minimal frameworks)
    - When middleware ecosystem compatibility with Express is required
    
    **Key patterns covered:**
    
    - Server setup with TypeScript type providers
    - Plugin system and encapsulation patterns
    - JSON Schema validation for request/response
    - Lifecycle hooks (onRequest, preHandler, onSend, etc.)
    - Decorators for extending Fastify/Request/Reply
    - Error handling with setErrorHandler
    - Route organization with prefix patterns
    
    ---
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Server setup, routes, schemas, error handling, testing
    - [examples/plugins.md](examples/plugins.md) - Plugin system, encapsulation, decorators
    - [examples/schemas.md](examples/schemas.md) - TypeBox schemas, validation, type-safe routes
    - [examples/hooks.md](examples/hooks.md) - Lifecycle hooks and cross-cutting concerns
    - [reference.md](reference.md) - Decision frameworks, anti-patterns, quick reference
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    **Schema-first, compiled validation.** Fastify compiles JSON schemas at startup into highly optimized validator functions. This provides both runtime safety and documentation from a single source of truth.
    
    **Plugin encapsulation creates microservices in a monolith.** Each plugin has its own scope for decorators and hooks. Child plugins inherit from parents, but parents cannot access child resources - enabling clean separation of concerns.
    
    **Performance without sacrifice.** Fastify achieves 2-3x throughput over Express while maintaining developer ergonomics through TypeScript integration and comprehensive hook system.
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Server Setup with Type Provider
    
    Configure Fastify with TypeBox for compile-time AND runtime type safety. `Type` is re-exported from `@fastify/type-provider-typebox`.
    
    ```typescript
    import Fastify from "fastify";
    import { Type, TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
    
    const SERVER_PORT = 3000;
    const SERVER_HOST = "0.0.0.0";
    
    const buildServer = () => {
      const server = Fastify({
        logger: { level: process.env.LOG_LEVEL ?? "info" },
      }).withTypeProvider<TypeBoxTypeProvider>();
    
      server.setErrorHandler(errorHandler);
      server.register(userRoutes, { prefix: "/api/users" });
    
      return server;
    };
    
    export { buildServer };
    ```
    
    **Why good:** TypeBox provider enables type inference from schemas, factory function enables testing, `Type` imported from same package
    
    > Full example with startup, error handling, and testing: [examples/core.md](examples/core.md)
    
    ---
    
    ### Pattern 2: Schema Definition with TypeBox
    
    Define schemas that provide both TypeScript types AND runtime validation from a single source.
    
    ```typescript
    import { Type, Static } from "@fastify/type-provider-typebox";
    
    const MIN_USERNAME_LENGTH = 3;
    const MAX_USERNAME_LENGTH = 50;
    
    export const UserSchema = Type.Object({
      id: Type.String({ format: "uuid" }),
      username: Type.String({
        minLength: MIN_USERNAME_LENGTH,
        maxLength: MAX_USERNAME_LENGTH,
      }),
      email: Type.String({ format: "email" }),
    });
    
    // Derive TypeScript types from schemas
    export type User = Static<typeof UserSchema>;
    ```
    
    **Why good:** Single source of truth for types and validation, `Static<>` derives TS types automatically
    
    > Full schema patterns (composition, partial updates, reusable components): [examples/schemas.md](examples/schemas.md)
    
    ---
    
    ### Pattern 3: Route Definition with Full Schema
    
    Define routes with request AND response schemas for complete type safety and serialization optimization.
    
    ```typescript
    import type { FastifyPluginAsync } from "fastify";
    import { Type } from "@fastify/type-provider-typebox";
    
    const HTTP_OK = 200;
    const HTTP_NOT_FOUND = 404;
    
    export const userRoutes: FastifyPluginAsync = async (fastify) => {
      fastify.get(
        "/:id",
        {
          schema: {
            params: UserParamsSchema,
            response: {
              [HTTP_OK]: UserSchema,
              [HTTP_NOT_FOUND]: ErrorSchema,
            },
          },
        },
        async (request, reply) => {
          const user = await fastify.userService.findById(request.params.id);
          if (!user) {
            return reply.status(HTTP_NOT_FOUND).send({
              statusCode: HTTP_NOT_FOUND,
              error: "Not Found",
              message: `User ${request.params.id} not found`,
            });
          }
          return reply.status(HTTP_OK).send(user);
        },
      );
    };
    ```
    
    **Why good:** Response schemas enable fast-json-stringify (2-3x faster), full type inference on request objects, HTTP constants prevent magic numbers
    
    > Complete CRUD routes with pagination: [examples/core.md](examples/core.md)
    
    ---
    
    ### Pattern 4: Plugin Encapsulation
    
    Default plugins are **encapsulated** - decorators stay within scope. Use `fastify-plugin` (`fp`) to break encapsulation for shared infrastructure.
    
    ```typescript
    // ENCAPSULATED - decorators only available within this plugin
    export const authRoutes: FastifyPluginAsync = async (fastify) => {
      fastify.decorate("authConfig", { tokenExpiry: 3600 });
      // authConfig only accessible in this plugin
    };
    
    // SHARED - decorators exposed to parent scope
    import fp from "fastify-plugin";
    
    declare module "fastify" {
      interface FastifyInstance {
        config: AppConfig;
      }
    }
    
    const configPlugin: FastifyPluginAsync = async (fastify) => {
      fastify.decorate("config", { apiVersion: "v1" });
    };
    
    export const appConfig = fp(configPlugin, {
      name: "app-config",
      dependencies: [],
    });
    ```
    
    **Why good:** Domain plugins stay isolated, shared utilities use `fp()` to expose decorators, TypeScript augmentation provides type safety
    
    > Full plugin examples with dependencies, registration order: [examples/plugins.md](examples/plugins.md)
    
    ---
    
    ### Pattern 5: Lifecycle Hooks
    
    Use hooks for cross-cutting concerns at specific lifecycle points.
    
    **Hook execution order:**
    
    1. `onRequest` - Before parsing (request ID, timing)
    2. `preParsing` - Transform request stream
    3. `preValidation` - Before schema validation
    4. `preHandler` - After validation (auth, authorization)
    5. `preSerialization` - Transform response object
    6. `onSend` - Final payload modification
    7. `onResponse` - After response sent (metrics, logging)
    8. `onError` - On error (error logging)
    
    ```typescript
    // Plugin-level: applies to ALL routes in this plugin
    fastify.addHook("preHandler", requireAuth);
    
    // Route-level: applies to single route
    fastify.delete(
      "/users/:id",
      {
        preHandler: [requireAuth, requireAdmin],
      },
      async (request) => {
        /* ... */
      },
    );
    ```
    
    **Why good:** Plugin-level for consistent protection, route-level for selective application, hooks execute in array order
    
    > Full hook examples (request timing, auth, response headers, error logging): [examples/hooks.md](examples/hooks.md)
    
    ---
    
    ### Pattern 6: Error Handling
    
    Implement centralized error handling with `setErrorHandler`. Fastify validation errors have a `.validation` array (not `.message`).
    
    ```typescript
    import type { FastifyError, FastifyReply, FastifyRequest } from "fastify";
    
    const HTTP_BAD_REQUEST = 400;
    const HTTP_INTERNAL_ERROR = 500;
    
    export const errorHandler = (
      error: FastifyError,
      request: FastifyRequest,
      reply: FastifyReply,
    ) => {
      if (error.validation) {
        return reply.status(HTTP_BAD_REQUEST).send({
          statusCode: HTTP_BAD_REQUEST,
          error: "Bad Request",
          message: "Validation failed",
          details: error.validation,
        });
      }
    
      request.log.error(
        { error: error.message, stack: error.stack },
        "Unexpected error",
      );
      return reply.status(HTTP_INTERNAL_ERROR).send({
        statusCode: HTTP_INTERNAL_ERROR,
        error: "Internal Server Error",
        message: "An unexpected error occurred",
      });
    };
    ```
    
    **Why good:** Validation errors expose details, unexpected errors logged with stack but hidden from client
    
    > Full error handler with custom error classes: [examples/core.md](examples/core.md)
    
    ---
    
    ### Pattern 7: Decorators
    
    Extend Fastify instance, Request, and Reply with decorators.
    
    ```typescript
    // Instance decorator - services/utilities
    fastify.decorate("myService", serviceInstance);
    
    // Request decorator - per-request state (initialize with null, set in hook)
    fastify.decorateRequest("userId", null);
    fastify.addHook("preHandler", async (request) => {
      request.userId = decoded.userId;
    });
    
    // Reply decorator - response helpers (use function for `this` binding)
    fastify.decorateReply(
      "notFound",
      function (this: FastifyReply, message: string) {
        this.status(HTTP_NOT_FOUND).send({
          statusCode: HTTP_NOT_FOUND,
          error: "Not Found",
          message,
        });
      },
    );
    ```
    
    **CRITICAL:** Never use reference types (objects, arrays) as initial decorator values - they are **shared across ALL requests**. Use `null` and set per-request in hooks.
    
    > Full decorator examples: [examples/plugins.md](examples/plugins.md)
    
    ---
    
    ### Pattern 8: Testing with server.inject()
    
    Use the factory pattern for test isolation and `server.inject()` for zero-network-overhead testing.
    
    ```typescript
    import { buildServer } from "./server";
    
    let server: ReturnType<typeof buildServer>;
    
    beforeEach(async () => {
      server = buildServer();
      await server.ready();
    });
    
    afterEach(async () => {
      await server.close();
    });
    
    it("should list users", async () => {
      const response = await server.inject({
        method: "GET",
        url: "/api/users",
        query: { limit: "10" },
      });
    
      expect(response.statusCode).toBe(200);
      expect(response.json()).toHaveProperty("users");
    });
    ```
    
    **Why good:** `server.inject()` tests without network, `beforeEach`/`afterEach` ensures clean state, tests validation and success paths
    
    </patterns>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    ### High Priority Issues
    
    - **No type provider configured** - Loses compile-time type safety on request/response
    - **Shared plugins without `fastify-plugin`** - Decorators invisible to other plugins
    - **Missing response schemas** - Loses 2-3x serialization performance AND risks data leaks
    - **Raw status code numbers** - Use named constants (`HTTP_OK`, `HTTP_NOT_FOUND`)
    - **Reference types in `decorateRequest`/`decorateReply`** - Shared mutable state across ALL requests (security risk)
    
    ### Medium Priority Issues
    
    - **No error handler configured** - Stack traces exposed to clients in production
    - **Missing `dependencies` in plugin options** - Race conditions on decorator access
    - **No schema for query/params** - No validation, types are `unknown`
    - **Inline route handlers in god files** - Use modular route plugins with prefix
    
    ### Common Mistakes
    
    - **Forgetting `await server.ready()`** - Plugins may not be fully loaded
    - **Not cleaning up in `onClose`** - Connection leaks on shutdown
    - **Mixing async/await with `done` callback** - Pick one pattern per hook (causes double-completion)
    - **Using Express patterns** - `res.send()` vs `reply.send()`, `next()` vs returning
    
    ### Gotchas & Edge Cases
    
    - **Hook return values:** Returning a value from hooks sends response immediately (short-circuits)
    - **Plugin registration order:** Later plugins can't access earlier encapsulated decorators
    - **Validation error shape:** Fastify validation errors have `.validation` array, not `.message`
    - **Route specificity:** More specific routes must be registered before wildcards
    - **preHandler order:** Route-level runs AFTER plugin-level hooks
    - **onResponse timing:** Runs after response sent, cannot modify response
    - **Schema compilation:** Happens at startup, errors surface during `server.ready()`
    - **v5 redirect order:** `reply.redirect(url, statusCode)` not `reply.redirect(statusCode, url)` (reversed from v4)
    - **v5 reply.sent:** Use `reply.hijack()` instead of setting `reply.sent = true`
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST use `withTypeProvider<>()` for type-safe request/response handling)**
    
    **(You MUST wrap shared plugins with `fastify-plugin` to expose decorators to parent scope)**
    
    **(You MUST define response schemas to enable fast-json-stringify optimization)**
    
    **(You MUST use named constants for HTTP status codes - never raw numbers)**
    
    **Failure to follow these rules will break type safety and lose performance benefits.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related