api-framework-fastify ยท diff
git:20260202.b236384 to git:20260316.5c62783
132 added, 664 removed. Audit A to A.
---
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. Choose TypeBox or Zod type providers for end-to-end type safety. Plugin system enables modular, testable architecture.
+ > **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
+ **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
- - Response serialization for performance
- - Async handler patterns
+ ---
+
**Detailed Resources:**
- - For code examples:
- - [plugins.md](examples/plugins.md) - Plugin system, encapsulation, decorators
- - [schemas.md](examples/schemas.md) - TypeBox schemas, validation, type-safe routes
- - [hooks.md](examples/hooks.md) - Lifecycle hooks and cross-cutting concerns
- - For decision frameworks and anti-patterns, see [reference.md](reference.md)
+ - [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.
**Use Fastify when:**
- Building production APIs requiring high throughput
- Need schema validation at the framework level
- Want plugin architecture for large codebases
- Require fine-grained lifecycle control
**Use alternatives when:**
- Simple internal endpoints (your existing patterns)
- Edge deployments with bundle size constraints
- Need Express middleware ecosystem compatibility
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Server Setup with Type Provider
- Configure Fastify with TypeBox for compile-time AND runtime type safety.
-
- #### Constants
-
- ```typescript
- const SERVER_PORT = 3000;
- const SERVER_HOST = "0.0.0.0";
- ```
-
- #### Implementation
+ Configure Fastify with TypeBox for compile-time AND runtime type safety. `Type` is re-exported from `@fastify/type-provider-typebox`.
```typescript
- // src/server.ts
import Fastify from "fastify";
- import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
+ import { Type, 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",
- },
+ 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 };
+ export { buildServer };
```
- **Why good:** TypeBox provider enables type inference from schemas, factory function enables testing, logger configured from environment
-
- ```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 good:** TypeBox provider enables type inference from schemas, factory function enables testing, `Type` imported from same package
- **Why bad:** No type safety on request/response, magic number for port, no error handling for startup
+ > 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.
+ Define schemas that provide both TypeScript types AND runtime validation from a single source.
```typescript
- // src/schemas/user.ts
- import { Type, Static } from "@sinclair/typebox";
+ import { Type, Static } from "@fastify/type-provider-typebox";
const MIN_USERNAME_LENGTH = 3;
const MAX_USERNAME_LENGTH = 50;
- const MIN_AGE = 0;
- const MAX_AGE = 150;
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 })),
- createdAt: Type.String({ format: "date-time" }),
});
- export const CreateUserSchema = Type.Object({
- 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 })),
- });
-
- export const UserParamsSchema = Type.Object({
- id: Type.String({ format: "uuid" }),
- });
-
- export const UsersQuerySchema = Type.Object({
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),
- offset: Type.Optional(Type.Integer({ minimum: 0, default: 0 })),
- });
-
// Derive TypeScript types from schemas
export type User = Static<typeof UserSchema>;
- export type CreateUser = Static<typeof CreateUserSchema>;
- export type UserParams = Static<typeof UserParamsSchema>;
- export type UsersQuery = Static<typeof UsersQuerySchema>;
```
- **Why good:** Single source of truth for types and validation, named constants for constraints, Static<> derives TS types automatically
-
- ```typescript
- // WRONG: Separate types and validation
- interface User {
- id: string;
- username: string;
- }
-
- const validateUser = (data: unknown) => {
- if (typeof data.username !== "string") throw new Error("Invalid");
- if (data.username.length < 3) throw new Error("Too short");
- // Manual validation...
- };
- ```
+ **Why good:** Single source of truth for types and validation, `Static<>` derives TS types automatically
- **Why bad:** Types and validation can drift apart, no JSON Schema for documentation, manual validation is error-prone
+ > 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
- // src/routes/users.ts
import type { FastifyPluginAsync } from "fastify";
- import { Type } from "@sinclair/typebox";
-
- import {
- UserSchema,
- CreateUserSchema,
- UserParamsSchema,
- UsersQuerySchema,
- } from "../schemas/user";
+ import { Type } from "@fastify/type-provider-typebox";
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);
-
+ 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 ${id} 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) => {
- 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
+ **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 System with Encapsulation
+ ---
- Create encapsulated plugins for domain isolation, use fastify-plugin for shared utilities.
+ ### Pattern 4: Plugin Encapsulation
- #### Encapsulated Domain Plugin
+ Default plugins are **encapsulated** - decorators stay within scope. Use `fastify-plugin` (`fp`) to break encapsulation for shared infrastructure.
```typescript
- // src/plugins/auth-routes.ts
- import type { FastifyPluginAsync } from "fastify";
-
- // This plugin is ENCAPSULATED - decorators stay within this scope
+ // ENCAPSULATED - decorators only available within this plugin
export const authRoutes: FastifyPluginAsync = async (fastify) => {
- // This decorator is only available within authRoutes
- fastify.decorate("authConfig", {
- tokenExpiry: 3600,
- refreshExpiry: 86400,
- });
-
- fastify.post("/login", async (request, reply) => {
- // fastify.authConfig available here
- return { token: "..." };
- });
-
- fastify.post("/refresh", async (request, reply) => {
- return { token: "..." };
- });
+ fastify.decorate("authConfig", { tokenExpiry: 3600 });
+ // authConfig only accessible in this plugin
};
- ```
- #### Shared Plugin with fastify-plugin
-
- ```typescript
- // src/plugins/config.ts
+ // SHARED - decorators exposed to parent scope
import fp from "fastify-plugin";
- import type { FastifyPluginAsync } from "fastify";
- type AppConfig = {
- apiVersion: string;
- environment: string;
- };
-
- // Augment Fastify types
declare module "fastify" {
interface FastifyInstance {
config: AppConfig;
}
}
const configPlugin: FastifyPluginAsync = async (fastify) => {
- const config: AppConfig = {
- apiVersion: process.env.API_VERSION ?? "v1",
- environment: process.env.NODE_ENV ?? "development",
- };
-
- // This decorator is exposed to PARENT scope due to fastify-plugin
- fastify.decorate("config", config);
+ fastify.decorate("config", { apiVersion: "v1" });
};
- // Wrap with fastify-plugin to break encapsulation
export const appConfig = fp(configPlugin, {
name: "app-config",
- dependencies: [], // Declare plugin dependencies
+ dependencies: [],
});
```
- **Why good:** Domain plugins stay isolated (encapsulated), shared utilities use fastify-plugin to expose decorators upward, TypeScript augmentation provides type safety
-
- ```typescript
- // WRONG: Shared decorator without fastify-plugin
- import type { FastifyPluginAsync } from "fastify";
-
- export const configPlugin: FastifyPluginAsync = async (fastify) => {
- fastify.decorate("config", { apiVersion: "v1" });
- };
-
- // In another file:
- server.register(configPlugin);
- server.register(async (fastify) => {
- // fastify.config is undefined! Encapsulation blocks access
- });
- ```
+ **Why good:** Domain plugins stay isolated, shared utilities use `fp()` to expose decorators, TypeScript augmentation provides type safety
- **Why bad:** Without fastify-plugin, decorators are encapsulated and invisible to sibling/parent contexts
+ > Full plugin examples with dependencies, registration order: [examples/plugins.md](examples/plugins.md)
---
### Pattern 5: Lifecycle Hooks
- Use hooks for cross-cutting concerns at appropriate lifecycle points.
-
- ```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;
- }
- }
-
- const requestContextPlugin: FastifyPluginAsync = async (fastify) => {
- // onRequest: First hook, before 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");
- });
-
- // preHandler: After validation, before handler
- fastify.addHook("preHandler", async (request) => {
- // Add request ID to all log entries
- request.log = request.log.child({ requestId: request.requestId });
- });
+ Use hooks for cross-cutting concerns at specific lifecycle points.
- // onResponse: After response sent (for metrics/logging)
- fastify.addHook("onResponse", async (request, reply) => {
- const duration = process.hrtime.bigint() - request.startTime;
- const durationMs = Number(duration) / 1e6;
+ **Hook execution order:**
- request.log.info(
- {
- requestId: request.requestId,
- statusCode: reply.statusCode,
- durationMs,
- },
- "Request completed",
- );
- });
+ 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)
- // onError: Custom error logging
- fastify.addHook("onError", async (request, reply, error) => {
- request.log.error(
- {
- requestId: request.requestId,
- error: error.message,
- stack: error.stack,
- },
- "Request error",
- );
- });
- };
+ ```typescript
+ // Plugin-level: applies to ALL routes in this plugin
+ fastify.addHook("preHandler", requireAuth);
- export const requestContext = fp(requestContextPlugin, {
- name: "request-context",
- });
+ // Route-level: applies to single route
+ fastify.delete(
+ "/users/:id",
+ {
+ preHandler: [requireAuth, requireAdmin],
+ },
+ async (request) => {
+ /* ... */
+ },
+ );
```
- **Why good:** onRequest for early setup, preHandler for validation-dependent logic, onResponse for metrics (non-blocking), onError for centralized error logging
-
- **Hook execution order:**
+ **Why good:** Plugin-level for consistent protection, route-level for selective application, hooks execute in array order
- 1. `onRequest` - First, before body parsing
- 2. `preParsing` - Transform request stream
- 3. `preValidation` - Before schema validation
- 4. `preHandler` - After validation, before handler
- 5. `preSerialization` - Transform response before stringify
- 6. `onSend` - Final payload modification
- 7. `onResponse` - After response sent (async, non-blocking)
+ > 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 proper status codes and logging.
+ Implement centralized error handling with `setErrorHandler`. Fastify validation errors have a `.validation` array (not `.message`).
```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,
) => {
- const requestId = (request as { requestId?: string }).requestId;
-
- // Fastify validation errors
if (error.validation) {
- request.log.warn(
- { requestId, 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({ requestId, 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(
- { requestId, error: error.message, stack: error.stack },
+ { 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 7: Decorators for Service Layer
-
- Use decorators to inject services and utilities into the Fastify instance.
-
- ```typescript
- // src/plugins/services.ts
- import fp from "fastify-plugin";
- import type { FastifyPluginAsync } from "fastify";
-
- // Service interface - implement according to your domain
- interface ServiceDependencies {
- // Your service dependencies here
- }
-
- // Augment Fastify types with your services
- declare module "fastify" {
- interface FastifyInstance {
- // Add your service decorators here
- myService: ServiceDependencies;
- }
- }
-
- const servicesPlugin: FastifyPluginAsync = async (fastify) => {
- // Initialize your services
- const myService: ServiceDependencies = {
- // Your service implementation
- };
-
- // Expose via decorator
- fastify.decorate("myService", myService);
- };
-
- export const services = fp(servicesPlugin, {
- name: "services",
- dependencies: [], // List any plugin dependencies
- });
- ```
-
- ```typescript
- // src/server.ts - Registration order matters!
- import { appConfig } from "./plugins/config";
- import { services } from "./plugins/services";
- import { userRoutes } from "./routes/users";
-
- const buildServer = () => {
- const server = Fastify().withTypeProvider<TypeBoxTypeProvider>();
-
- // Register in dependency order
- server.register(appConfig);
- server.register(services);
-
- server.register(userRoutes, { prefix: "/api/users" });
-
- return server;
- };
- ```
+ **Why good:** Validation errors expose details, unexpected errors logged with stack but hidden from client
- **Why good:** Dependencies declared explicitly, TypeScript augmentation provides type safety, fastify-plugin exposes to all routes
+ > Full error handler with custom error classes: [examples/core.md](examples/core.md)
---
- ### Pattern 8: Request/Reply Decorators
-
- Extend Request and Reply with per-request utilities.
-
- ```typescript
- // src/plugins/request-utilities.ts
- import fp from "fastify-plugin";
- import type { FastifyPluginAsync } from "fastify";
-
- declare module "fastify" {
- interface FastifyRequest {
- userId: string | null;
- hasPermission: (permission: string) => boolean;
- }
-
- interface FastifyReply {
- notFound: (message: string) => void;
- badRequest: (message: string, details?: unknown) => void;
- }
- }
-
- const HTTP_BAD_REQUEST = 400;
- const HTTP_NOT_FOUND = 404;
-
- const requestUtilitiesPlugin: FastifyPluginAsync = async (fastify) => {
- // Request decorators - use getter for per-request state
- fastify.decorateRequest("userId", null);
-
- // Use getter for methods that need access to request state
- fastify.decorateRequest("hasPermission", function (permission: string) {
- // 'this' is the FastifyRequest instance
- const permissions = (this as { permissions?: string[] }).permissions ?? [];
- return permissions.includes(permission);
- });
-
- // Reply decorators - use getter pattern for methods
- 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 } : {}),
- });
- },
- );
- };
-
- export const requestUtilities = fp(requestUtilitiesPlugin, {
- name: "request-utilities",
- });
- ```
+ ### Pattern 7: Decorators
- **Usage in routes:**
+ Extend Fastify instance, Request, and Reply with decorators.
```typescript
- fastify.get("/:id", async (request, reply) => {
- if (!request.hasPermission("users:read")) {
- return reply.badRequest("Insufficient permissions");
- }
-
- const user = await fastify.userService.findById(request.params.id);
-
- if (!user) {
- return reply.notFound(`User ${request.params.id} not found`);
- }
+ // Instance decorator - services/utilities
+ fastify.decorate("myService", serviceInstance);
- return user;
+ // Request decorator - per-request state (initialize with null, set in hook)
+ fastify.decorateRequest("userId", null);
+ fastify.addHook("preHandler", async (request) => {
+ request.userId = decoded.userId;
});
- ```
- **Why good:** Reply helpers reduce boilerplate, permission check uses request context, TypeScript augmentation provides autocomplete
-
- ---
-
- ### Pattern 9: Route-Level Hooks
-
- Apply hooks to specific routes for authentication, authorization, or validation.
-
- ```typescript
- // src/routes/admin.ts
- import type { FastifyPluginAsync } from "fastify";
- import type { FastifyRequest, FastifyReply } from "fastify";
-
- const HTTP_UNAUTHORIZED = 401;
- const HTTP_FORBIDDEN = 403;
-
- // Route-level preHandler for authentication
- const requireAuth = async (request: FastifyRequest, reply: FastifyReply) => {
- const token = request.headers.authorization?.replace("Bearer ", "");
-
- if (!token) {
- return reply.status(HTTP_UNAUTHORIZED).send({
- statusCode: HTTP_UNAUTHORIZED,
- error: "Unauthorized",
- message: "Authentication required",
- });
- }
-
- const user = await request.server.authService.validateToken(token);
-
- if (!user) {
- return reply.status(HTTP_UNAUTHORIZED).send({
- statusCode: HTTP_UNAUTHORIZED,
- error: "Unauthorized",
- message: "Invalid token",
- });
- }
-
- request.userId = user.id;
- };
-
- // Route-level preHandler for authorization
- const requireAdmin = async (request: FastifyRequest, reply: FastifyReply) => {
- const user = await request.server.userService.findById(request.userId!);
-
- if (user?.role !== "admin") {
- return reply.status(HTTP_FORBIDDEN).send({
- statusCode: HTTP_FORBIDDEN,
- error: "Forbidden",
- message: "Admin access required",
+ // 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,
});
- }
- };
-
- export const adminRoutes: FastifyPluginAsync = async (fastify) => {
- // Apply auth to all routes in this plugin
- fastify.addHook("preHandler", requireAuth);
-
- // GET /admin/users - requires auth only
- fastify.get("/users", async () => {
- return fastify.userService.listAll();
- });
-
- // DELETE /admin/users/:id - requires auth AND admin
- fastify.delete(
- "/users/:id",
- {
- preHandler: [requireAdmin], // Additional route-level hook
- },
- async (request) => {
- const { id } = request.params as { id: string };
- await fastify.userService.delete(id);
- return { success: true };
- },
- );
- };
+ },
+ );
```
- **Why good:** Plugin-level hook applies to all routes, route-specific hooks for additional checks, hooks can short-circuit with reply
+ **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 10: Testing Patterns
+ ### Pattern 8: Testing with server.inject()
- Structure tests using the buildServer factory pattern.
+ Use the factory pattern for test isolation and `server.inject()` for zero-network-overhead testing.
```typescript
- // src/server.test.ts
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
-
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");
- });
+ let server: ReturnType<typeof buildServer>;
- 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",
- },
- });
+ beforeEach(async () => {
+ server = buildServer();
+ await server.ready();
+ });
- expect(response.statusCode).toBe(201);
+ afterEach(async () => {
+ await server.close();
+ });
- const body = response.json();
- expect(body.username).toBe("testuser");
- expect(body.id).toBeDefined();
+ it("should list users", async () => {
+ const response = await server.inject({
+ method: "GET",
+ url: "/api/users",
+ query: { limit: "10" },
});
- 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);
- });
+ expect(response.statusCode).toBe(200);
+ expect(response.json()).toHaveProperty("users");
});
```
- **Why good:** server.inject() tests without network overhead, beforeEach/afterEach ensures clean state, tests validation and success paths
+ **Why good:** `server.inject()` tests without network, `beforeEach`/`afterEach` ensures clean state, tests validation and success paths
</patterns>
---
- <integration>
-
- ## Integration Guide
-
- Fastify integrates with your existing infrastructure through its plugin system.
-
- **External Resources:**
+ <red_flags>
- - Create plugins that decorate `fastify` with external clients
- - Use `onClose` hook for connection cleanup
- - Wrap with `fastify-plugin` to expose to all routes
+ ## RED FLAGS
- **Authentication:**
+ ### High Priority Issues
- - Implement auth logic in `preHandler` hooks
- - Use request decorators for user state (`request.userId`)
- - Plugin-level hooks for route groups, route-level for specific endpoints
+ - **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)
- **Logging:**
+ ### Medium Priority Issues
- - Fastify's built-in logger provides high-performance JSON logging
- - Configure via server options: `logger: { level: 'info' }`
- - Use `request.log` for request-scoped logging with automatic request ID
+ - **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
- **Caching:**
+ ### Common Mistakes
- - Use `onSend` hook to add Cache-Control headers
- - Implement ETag support in `preSerialization`
- - Response schemas enable efficient serialization
+ - **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
- **API Documentation:**
+ ### Gotchas & Edge Cases
- - Use `@fastify/swagger` with `@fastify/swagger-ui`
- - Schemas are automatically extracted from route definitions
- - TypeBox schemas generate OpenAPI-compatible JSON Schema
+ - **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()`
- </integration>
+ </red_flags>
---
<critical_reminders>
## CRITICAL REMINDERS
-
- **Before implementing Fastify routes, verify:**
> **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>