src/skills/api-framework-fastify/SKILL.md
Fastify routes, JSON Schema validation, plugin system, TypeScript type providers
npx skillsauth add agents-inc/skills api-framework-fastifyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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-typeboxfor end-to-end type safety (bothTypeandTypeBoxTypeProviderre-exported from it). Wrap shared plugins withfastify-pluginto expose decorators. Always define response schemas for serialization performance and data leak prevention.
<critical_requirements>
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:
When NOT to use:
Key patterns covered:
Detailed Resources:
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>Configure Fastify with TypeBox for compile-time AND runtime type safety. Type is re-exported from @fastify/type-provider-typebox.
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
Define schemas that provide both TypeScript types AND runtime validation from a single source.
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
Define routes with request AND response schemas for complete type safety and serialization optimization.
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
Default plugins are encapsulated - decorators stay within scope. Use fastify-plugin (fp) to break encapsulation for shared infrastructure.
// 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
Use hooks for cross-cutting concerns at specific lifecycle points.
Hook execution order:
onRequest - Before parsing (request ID, timing)preParsing - Transform request streampreValidation - Before schema validationpreHandler - After validation (auth, authorization)preSerialization - Transform response objectonSend - Final payload modificationonResponse - After response sent (metrics, logging)onError - On error (error logging)// 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
Implement centralized error handling with setErrorHandler. Fastify validation errors have a .validation array (not .message).
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
Extend Fastify instance, Request, and Reply with decorators.
// 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
Use the factory pattern for test isolation and server.inject() for zero-network-overhead testing.
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
<red_flags>
fastify-plugin - Decorators invisible to other pluginsHTTP_OK, HTTP_NOT_FOUND)decorateRequest/decorateReply - Shared mutable state across ALL requests (security risk)dependencies in plugin options - Race conditions on decorator accessunknownawait server.ready() - Plugins may not be fully loadedonClose - Connection leaks on shutdowndone callback - Pick one pattern per hook (causes double-completion)res.send() vs reply.send(), next() vs returning.validation array, not .messageserver.ready()reply.redirect(url, statusCode) not reply.redirect(statusCode, url) (reversed from v4)reply.hijack() instead of setting reply.sent = true</red_flags>
<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>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety