### Install @fastify/autoload Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/autoload.md Install the @fastify/autoload package using npm. ```bash npm install @fastify/autoload ``` -------------------------------- ### Install @fastify/auth Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/authentication.md Installs the @fastify/auth plugin, which allows composing multiple authentication strategies. ```bash npm install @fastify/auth ``` -------------------------------- ### Install close-with-grace Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/configuration.md Install the `close-with-grace` package to handle graceful shutdown signals and errors. ```bash npm install close-with-grace ``` -------------------------------- ### Install Fastify Skills CLI or Copy Directly Source: https://context7.com/thecodepace/fastify-skills/llms.txt Instructions for installing Fastify Skills using the `skills` CLI or by copying the skill directory directly for use with AI agents like Claude Code. ```bash # Install via the skills CLI npx skills add TheCodePace/fastify-skills # Or copy directly for Claude Code cp -r skills/fastify-best-practise ~/.claude/skills/ ``` -------------------------------- ### Install Fastify Skills Source: https://github.com/thecodepace/fastify-skills/blob/main/README.md Use this command to add the Fastify Skills to your AI coding agent. ```bash npx skills add TheCodePace/fastify-skills ``` -------------------------------- ### BuildServer Pattern for Test Setup Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/testing.md Illustrates the `buildServer` pattern for creating a reusable Fastify server instance for testing. This pattern simplifies test setup by abstracting server configuration and plugin autoloading. The `createTestServer` helper function is provided for convenience. ```typescript // src/server.ts import path from "node:path"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import autoload from "@fastify/autoload"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); interface ServerOptions { logger?: boolean; } export function buildServer(options: ServerOptions = {}) { const server = Fastify({ logger: options.logger ?? false, }); // Autoload shared plugins server.register(autoload, { dir: path.join(__dirname, "plugins"), }); // Autoload routes server.register(autoload, { dir: path.join(__dirname, "routes"), autoHooks: true, cascadeHooks: true, }); return server; } ``` ```typescript // test/helpers.ts import { buildServer } from "../src/server.js"; export function createTestServer() { return buildServer({ logger: false }); } ``` -------------------------------- ### Install Zod and Fastify Type Provider Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/schema-validation-zod.md Install the necessary packages for Zod integration with Fastify. ```bash npm install zod fastify-type-provider-zod ``` -------------------------------- ### Install @fastify/sensible Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/error-handling.md Install the @fastify/sensible plugin using npm to add convenient HTTP error methods and utilities to your Fastify instance. ```bash npm install @fastify/sensible ``` -------------------------------- ### Fastify Application Entry Point Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/configuration.md This is the main entry point for the Fastify application, responsible for building the server and starting it to listen on the configured port and host. ```typescript import { envSchema } from "./schema/env.js"; import { buildServer } from "./server.js"; const config = envSchema.parse(process.env); const server = buildServer({ config }); await server.listen({ port: config.PORT, host: config.HOST, }); ``` -------------------------------- ### Use Pretty Printing in Development Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/logging.md Install `pino-pretty` and configure it as a transport for the logger. This provides human-readable output in development environments. ```bash npm install --save-dev pino-pretty ``` ```typescript import Fastify from "fastify"; const isDev = process.env.NODE_ENV !== "production"; const server = Fastify({ logger: { level: "debug", transport: isDev ? { target: "pino-pretty", options: { translateTime: "HH:MM:ss Z", ignore: "pid,hostname", }, } : undefined, }, }); ``` -------------------------------- ### Install @fastify/jwt Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/authentication.md Install the `@fastify/jwt` package to enable JWT authentication capabilities in your Fastify application. ```bash npm install @fastify/jwt ``` -------------------------------- ### Automatic Plugin and Route Loading with @fastify/autoload Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/autoload.md This example demonstrates how to use @fastify/autoload to automatically load plugins and routes. It simplifies registration and leverages directory structure for route prefixes. ```typescript import path from "node:path"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import autoload from "@fastify/autoload"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); function buildServer() { const server = Fastify({ logger: true }); // Load all plugins from plugins/ — these use fastify-plugin, so they're shared server.register(autoload, { dir: path.join(__dirname, "plugins"), }); // Load all routes from routes/ — each folder becomes a prefix automatically server.register(autoload, { dir: path.join(__dirname, "routes"), options: { prefix: "/api" }, // optional: add a global prefix autoHooks: true, // load _hooks files automatically cascadeHooks: true, // propagate hooks to child directories }); return server; } ``` -------------------------------- ### Define User Routes with Zod Schemas Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/schema-validation-zod.md Define user-related routes using `FastifyPluginAsyncZod` for full type inference. This example demonstrates GET and POST requests with Zod schemas for query parameters, request bodies, and response payloads, ensuring type safety for route handlers. ```typescript import { FastifyPluginAsyncZod } from "fastify-type-provider-zod"; import { z } from "zod"; const userSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email(), }); const userRoutes: FastifyPluginAsyncZod = async function (fastify) { fastify.get( "/", { schema: { querystring: z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), }), response: { 200: z.array(userSchema), }, }, }, async (request, reply) => { // request.query.page and request.query.limit are fully typed as numbers const { page, limit } = request.query; return getUsers(page, limit); }, ); fastify.post( "/", { schema: { body: z.object({ name: z.string().min(1), email: z.string().email(), }), response: { 201: userSchema, }, }, }, async (request, reply) => { // request.body is typed: { name: string; email: string } const user = await createUser(request.body); reply.status(201); return user; }, ); fastify.get( "/:id", { schema: { params: z.object({ id: z.string().uuid() }), response: { 200: userSchema, }, }, }, async (request, reply) => { // request.params.id is typed as string const user = await findUser(request.params.id); if (!user) { throw fastify.httpErrors.notFound( `User ${request.params.id} not found`, ); } return user; }, ); }; export default userRoutes; ``` -------------------------------- ### Fastify Server Setup with Autoload Plugins Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/authentication.md Configure the Fastify server to automatically load plugins and routes. Ensure that authentication plugins are loaded before routes that require authentication. ```typescript import Fastify from "fastify"; import autoload from "@fastify/autoload"; import { join } from "node:path"; function buildServer(options = {}) { const server = Fastify({ logger: options.logger || false }); server.register(autoload, { dir: join(import.meta.dirname, "plugins"), }); server.register(autoload, { dir: join(import.meta.dirname, "routes"), autoHooks: true, cascadeHooks: true, }); return server; } export default buildServer; ``` -------------------------------- ### Fastify Route: List and Create Users Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/autoload.md Defines handlers for listing all users (GET /) and creating a new user (POST /) within the users route directory. ```typescript import { FastifyInstance } from "fastify"; async function userRoutes(fastify: FastifyInstance) { fastify.get("/", async (request, reply) => { return fastify.db.query("SELECT * FROM users"); }); fastify.post("/", async (request, reply) => { // create user }); } export default userRoutes; ``` -------------------------------- ### Manual Plugin and Route Registration (Incorrect) Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/autoload.md This example shows the verbose way of registering each plugin and route manually. It is prone to errors and harder to maintain as the application grows. ```typescript import Fastify from "fastify"; import dbPlugin from "./plugins/db.js"; import authPlugin from "./plugins/auth.js"; import configPlugin from "./plugins/config.js"; import userRoutes from "./routes/users/index.js"; import postRoutes from "./routes/posts/index.js"; import commentRoutes from "./routes/comments/index.js"; function buildServer() { const server = Fastify({ logger: true }); // Must manually add every new plugin and route server.register(dbPlugin); server.register(authPlugin); server.register(configPlugin); server.register(userRoutes, { prefix: "/users" }); server.register(postRoutes, { prefix: "/posts" }); server.register(commentRoutes, { prefix: "/comments" }); return server; } ``` -------------------------------- ### Verify External Dependencies Before Startup with onReady Hook Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/hooks-lifecycle.md The `onReady` hook runs after plugins are loaded but before the server starts listening. It's ideal for performing startup checks, like verifying database connectivity. ```typescript fastify.addHook("onReady", async () => { const healthy = await checkDatabaseConnection(); if (!healthy) { throw new Error("Database is not reachable — aborting startup"); } fastify.log.info("All dependencies healthy"); }); ``` -------------------------------- ### Install Zod for Environment Validation Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/configuration.md Install the Zod library to enable schema-driven validation and parsing of environment variables. This is a crucial step for centralizing and securing your application's configuration. ```bash npm install zod ``` -------------------------------- ### Testing with Fastify inject() using vitest Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/testing.md Demonstrates testing Fastify routes with `vitest` using the `inject()` method. This setup is efficient for integration tests. The `buildServer` function is used to create the server instance, and `beforeAll` hook initializes it. ```typescript import { describe, it, expect, beforeAll } from "vitest"; import { buildServer } from "../src/server.js"; import { FastifyInstance } from "fastify"; describe("User routes", () => { let server: FastifyInstance; beforeAll(() => { server = buildServer({ logger: false }); }); it("should return users list", async () => { const response = await server.inject({ method: "GET", url: "/users", }); expect(response.statusCode).toBe(200); expect(response.json()).toEqual(expect.arrayContaining([])); }); it("should create a user", async () => { const response = await server.inject({ method: "POST", url: "/users", payload: { name: "John Doe", email: "john@example.com", }, }); expect(response.statusCode).toBe(201); expect(response.json()).toMatchObject({ name: "John Doe", email: "john@example.com", }); }); it("should return 400 for invalid body", async () => { const response = await server.inject({ method: "POST", url: "/users", payload: { name: "", // invalid }, }); expect(response.statusCode).toBe(400); }); }); ``` -------------------------------- ### Fastify with Zod Type Provider Setup Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/schema-validation-zod.md Configure Fastify to use Zod for schema validation and serialization, enabling full type inference for request bodies and responses. ```typescript import Fastify from "fastify"; import { serializerCompiler, validatorCompiler, type ZodTypeProvider, } from "fastify-type-provider-zod"; import { z } from "zod"; const server = Fastify(); // Set up the Zod validator and serializer compilers server.setValidatorCompiler(validatorCompiler); server.setSerializerCompiler(serializerCompiler); // Use withTypeProvider for full type inference const app = server.withTypeProvider(); app.post( "/users", { schema: { body: z.object({ name: z.string().min(1), email: z.string().email(), }), response: { 201: z.object({ id: z.string().uuid(), name: z.string(), email: z.string(), }), }, }, }, async (request, reply) => { // request.body is fully typed: { name: string; email: string } const { name, email } = request.body; const user = await createUser(name, email); reply.status(201); return user; }, ); ``` -------------------------------- ### Incorrect: Testing with Real HTTP Requests Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/testing.md Avoid starting a real HTTP server in tests. This approach is slow, prone to flakiness, and dependent on available ports. ```typescript import { test } from "node:test"; test("GET /users", async ({ assert }) => { const server = buildServer(); await server.listen({ port: 0 }); const address = server.addresses()[0]; // WRONG: real HTTP requests are slow, flaky, and port-dependent const res = await fetch(`http://localhost:${address.port}/users`); const data = await res.json(); assert.ok(Array.isArray(data)); await server.close(); }); ``` -------------------------------- ### Automatic Route Loading with @fastify/autoload Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/route-best-practices.md Leverage @fastify/autoload for automatic loading of route plugins. This simplifies server setup by eliminating manual registration and imports for new resources. ```typescript import path from "node:path"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import autoload from "@fastify/autoload"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); function buildServer() { const server = Fastify({ logger: true }); server.register(autoload, { dir: path.join(__dirname, "plugins"), }); // Each folder in routes/ becomes a prefix: routes/v1/users/ → /api/v1/users server.register(autoload, { dir: path.join(__dirname, "routes"), options: { prefix: "/api" }, autoHooks: true, cascadeHooks: true, }); return server; } ``` -------------------------------- ### Get Users Source: https://context7.com/thecodepace/fastify-skills/llms.txt Retrieves a list of users with optional pagination. The `page` and `limit` query parameters are automatically validated and coerced to numbers. ```APIDOC ## GET / ### Description Retrieves a list of users with optional pagination. ### Method GET ### Endpoint / #### Query Parameters - **page** (number) - Optional - The page number to retrieve (defaults to 1). - **limit** (number) - Optional - The number of users per page (defaults to 20, max 100). #### Response ##### Success Response (200) - **array** (array) - An array of user objects. - **id** (string) - The user's unique identifier (UUID). - **name** (string) - The user's name. - **email** (string) - The user's email address. - **createdAt** (string) - The timestamp when the user was created (ISO 8601 format). ``` -------------------------------- ### Setup Schema Validation with Zod and fastify-type-provider-zod Source: https://context7.com/thecodepace/fastify-skills/llms.txt Configure Fastify to use Zod for schema validation and serialization. This plugin should be registered early in your application lifecycle. ```typescript // npm install zod fastify-type-provider-zod // src/plugins/zod.ts import fp from "fastify-plugin"; import { serializerCompiler, validatorCompiler } from "fastify-type-provider-zod"; export default fp(async function zodPlugin(fastify) { fastify.setValidatorCompiler(validatorCompiler); fastify.setSerializerCompiler(serializerCompiler); }, { name: "zod-plugin" }); ``` -------------------------------- ### Log Server Listening Address with onListen Hook Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/hooks-lifecycle.md After the server successfully starts listening, the `onListen` hook can be used to log information such as the server's address and port. ```typescript fastify.addHook("onListen", async () => { const address = fastify.server.address(); const addr = address && typeof address === "object" ? `${address.address}:${address.port}` : String(address); fastify.log.info(`Server listening at ${addr}`); }); ``` -------------------------------- ### Log Plugin Registration with onRegister Hook Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/hooks-lifecycle.md The `onRegister` hook is called every time a new plugin scope is registered using `fastify.register()`. This example logs the prefix of each registered plugin. ```typescript fastify.addHook("onRegister", (instance, opts) => { instance.log.debug({ prefix: opts.prefix }, "Plugin registered"); }); ``` -------------------------------- ### Correctly Encapsulating Routes with fastify-plugin for Shared Services Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/encapsulation.md This example shows how to correctly use `fastify-plugin` for shared services like a database connection, while keeping route handlers encapsulated. The `dbPlugin` uses `fastify-plugin` to be shared, while `userRoutes` does not, ensuring its decorators and hooks remain scoped. ```typescript import fp from "fastify-plugin"; import type { FastifyInstance } from "fastify"; interface DbPluginOptions { connectionString: string; } async function dbPlugin(fastify: FastifyInstance, options: DbPluginOptions) { const pool = createPool(options.connectionString); fastify.decorate("db", pool); fastify.addHook("onClose", async () => { await pool.end(); }); } export default fp(dbPlugin, { name: "db-plugin", fastify: "5.x", }); ``` ```typescript import { FastifyInstance } from "fastify"; // No fastify-plugin wrapper → encapsulated async function userRoutes(fastify: FastifyInstance) { // fastify.db is available because db-plugin used fastify-plugin fastify.get("/", async (request, reply) => { return fastify.db.query("SELECT * FROM users"); }); } export default userRoutes; ``` -------------------------------- ### Correctly Scoping Route-Specific Decorators Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/encapsulation.md This example demonstrates proper encapsulation by keeping route-specific decorators, like `isAdmin`, scoped within the route handler itself. By not using `fastify-plugin`, the decorator is only available to the `adminRoutes` plugin and its children. ```typescript // No fp wrapper — isAdmin stays scoped to admin routes async function adminRoutes(fastify: FastifyInstance) { fastify.decorate("isAdmin", (request) => { return request.user?.role === "admin"; }); fastify.addHook("preHandler", async (request, reply) => { if (!fastify.isAdmin(request)) { reply.status(403); return { error: "Forbidden" }; } }); fastify.get("/dashboard", async () => { return { stats: await getAdminStats() }; }); } export default adminRoutes; ``` -------------------------------- ### Correct Fastify Server Build Function Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/create-server.md Use a build function to create a customizable and reusable Fastify server. This function accepts options and uses @fastify/autoload to load plugins and routes, ensuring a consistent setup. ```typescript import path from "node:path"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import autoload from "@fastify/autoload"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); interface ServerOptions { logger?: boolean; } function buildServer(options: ServerOptions = {}) { const server = Fastify({ logger: options.logger || false, }); // Autoload shared plugins (use fastify-plugin inside each) server.register(autoload, { dir: path.join(__dirname, "plugins"), }); // Autoload routes (encapsulated, prefixes from folder names) server.register(autoload, { dir: path.join(__dirname, "routes"), autoHooks: true, cascadeHooks: true, }); return server; } const server = buildServer({ logger: true }); await server.listen({ port: 3000 }); ``` -------------------------------- ### Fastify Reject Unknown Content Type Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/content-type-parser.md This example shows the default behavior where Fastify rejects requests with unknown content types by returning a 415 Unsupported Media Type error. ```typescript // WRONG: only the default json/text parsers exist // Requests with other content types are rejected with 415 app.post("/data", async (request) => { return { received: request.body }; }); ``` -------------------------------- ### Test Fastify Routes with inject() Source: https://context7.com/thecodepace/fastify-skills/llms.txt Use the `inject()` method for testing routes without starting a real HTTP server. Always rely on the `buildServer` factory and use assertion libraries like vitest or `node:test`. ```typescript // test/helpers.ts import { buildServer } from "../src/server.js"; export const createTestServer = () => buildServer({ logger: false }); ``` ```typescript // test/routes/users.test.ts import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { createTestServer } from "../helpers.js"; import type { FastifyInstance } from "fastify"; describe("User routes", () => { let server: FastifyInstance; beforeAll(() => { server = createTestServer(); }); afterAll(async () => { await server.close(); }); it("GET /users returns array", async () => { const res = await server.inject({ method: "GET", url: "/users" }); expect(res.statusCode).toBe(200); expect(res.json()).toEqual(expect.arrayContaining([])); }); it("POST /users creates user (201)", async () => { const res = await server.inject({ method: "POST", url: "/users", payload: { name: "Jane Doe", email: "jane@example.com" } }); expect(res.statusCode).toBe(201); expect(res.json()).toMatchObject({ name: "Jane Doe", email: "jane@example.com" }); }); it("POST /users returns 400 for invalid body", async () => { const res = await server.inject({ method: "POST", url: "/users", payload: { name: "" } }); expect(res.statusCode).toBe(400); }); it("requires auth for protected routes", async () => { const noToken = await server.inject({ method: "GET", url: "/profile" }); expect(noToken.statusCode).toBe(401); const token = server.jwt.sign({ id: "user-1", role: "user" }); const withToken = await server.inject({ method: "GET", url: "/profile", headers: { authorization: `Bearer ${token}` } }); expect(withToken.statusCode).toBe(200); }); }); ``` -------------------------------- ### Setting Reply Status Code Before Returning (Correct) Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/error-handling.md Ensure the correct HTTP status code is set before returning the response. This example correctly sets the status to 201 (Created) for a successful POST request. ```typescript server.post("/users", async (request, reply) => { const user = await createUser(request.body); reply.status(201); return user; }); ``` -------------------------------- ### Implement Graceful Shutdown in Fastify Server Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/configuration.md Register `close-with-grace` within the `buildServer` function to ensure every server instance handles process signals and errors gracefully. This setup automatically listens for SIGINT, SIGTERM, uncaught exceptions, and unhandled rejections. ```typescript import Fastify from "fastify"; import type { Env } from "./schema/env.js"; export interface BuildServerOptions {} export function buildServer(_: BuildServerOptions) { const server = Fastify(); // Autoload plugins (config, db, auth — all use fastify-plugin) // Autoload routes (encapsulated, prefixes from folder names) // Graceful shutdown — close-with-grace handles SIGINT, SIGTERM, // uncaught exceptions, and unhandled rejections automatically closeWithGrace({ delay: 10_000 }, async ({ signal, err }) => { if (err) { server.log.error({ err }, "server closing with error"); } else { server.log.info(`${signal} received, server closing`); } await server.close(); }); return server; } ``` -------------------------------- ### Configuring Body Size Limits in Fastify Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/content-type-parser.md Set explicit body size limits globally, per content type, or per route to protect against large payload attacks. This example shows setting a global limit and a specific limit for JSON. ```typescript import Fastify from "fastify"; // Global limit const app = Fastify({ bodyLimit: 1048576, // 1 MB }); // Per content type limit app.addContentTypeParser("application/json", { parseAs: "string", bodyLimit: 2097152, // 2 MB for JSON }, async (request, body) => { return JSON.parse(body as string); }); // Per route limit app.post("/large-upload", { bodyLimit: 52428800, // 50 MB for this route only }, async (request) => { return { size: JSON.stringify(request.body).length }; }); ``` -------------------------------- ### Fastify Route: Get User's Posts Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/autoload.md Handles requests to retrieve all posts for a specific user (GET /). The user's ID is available as a route parameter. ```typescript import { FastifyInstance } from "fastify"; async function userPostsRoutes(fastify: FastifyInstance) { // URL is /users/:id/posts fastify.get("/", async (request, reply) => { const { id } = request.params as { id: string }; return fastify.db.query("SELECT * FROM posts WHERE user_id = $1", [id]); }); } export default userPostsRoutes; ``` -------------------------------- ### Recommended Fastify Project Structure Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/encapsulation.md Illustrates a typical Fastify project layout using @fastify/autoload for efficient plugin and route management. Plugins in `plugins/` are shared globally, while routes in `routes/` maintain encapsulation. ```tree src/ plugins/ # Autoloaded — shared plugins (use fastify-plugin) db.ts auth.ts cache.ts routes/ # Autoloaded — encapsulated route plugins (NO fastify-plugin) _hooks.ts # Hooks for all routes (with autoHooks: true) users/ index.ts # → /users _hooks.ts # Hooks for /users only schema.ts posts/ index.ts # → /posts schema.ts server.ts # buildServer with autoload ``` -------------------------------- ### Fastify Route: Get User by ID Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/autoload.md Handles requests to retrieve a single user by their ID (GET /). The `:id` parameter is automatically extracted from the URL based on the folder structure. ```typescript import { FastifyInstance } from "fastify"; async function userByIdRoutes(fastify: FastifyInstance) { // The :id param is part of the URL prefix from the folder name fastify.get("/", async (request, reply) => { const { id } = request.params as { id: string }; return fastify.db.query("SELECT * FROM users WHERE id = $1", [id]); }); fastify.put("/", async (request, reply) => { const { id } = request.params as { id: string }; // update user }); fastify.delete("/", async (request, reply) => { const { id } = request.params as { id: string }; // delete user }); } export default userByIdRoutes; ``` -------------------------------- ### Recommended Fastify Project Structure Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/SKILL.md Illustrates a standard directory layout for Fastify projects using `@fastify/autoload` for automatic plugin and route loading. This structure promotes modularity and organization. ```tree src/ plugins/ # Autoloaded — shared plugins (use fastify-plugin) db.ts auth.ts config.ts routes/ # Autoloaded — encapsulated route plugins (NO fastify-plugin) _hooks.ts # Global route hooks (with autoHooks: true) users/ index.ts # → /users _hooks.ts # Hooks for /users scope only schema.ts posts/ index.ts # → /posts schema.ts server.ts # buildServer() with autoload registration app.ts # Entry point — calls buildServer() and listen() test/ routes/ users.test.ts posts.test.ts helpers.ts # createTestServer() helper ``` -------------------------------- ### Build Fastify Server with Configuration and Autoloading Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/configuration.md Create a robust Fastify server factory that integrates environment configuration, logger settings based on NODE_ENV, and autoloads plugins and routes. It also includes graceful shutdown handling. ```typescript import path from "node:path"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import autoload from "@fastify/autoload"; import closeWithGrace from "close-with-grace"; import type { Env } from "./schema/env.js"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); const envToLogger = { development: { level: "debug", transport: { target: "pino-pretty", options: { translateTime: "HH:MM:ss Z", ignore: "pid,hostname", }, }, }, production: { level: "info", }, test: { level: "silent", }, } as const; export interface BuildServerOptions { config: Env; trustProxy?: boolean | string | number; } export function buildServer({ config, trustProxy }: BuildServerOptions) { const server = Fastify({ logger: envToLogger[config.NODE_ENV], trustProxy: trustProxy ?? false, requestTimeout: 120_000, bodyLimit: 1_048_576, return503OnClosing: true, forceCloseConnections: "idle", }); // Autoload plugins (config, db, auth — all use fastify-plugin) server.register(autoload, { dir: path.join(__dirname, "plugins"), }); // Autoload routes (encapsulated, prefixes from folder names) server.register(autoload, { dir: path.join(__dirname, "routes"), autoHooks: true, cascadeHooks: true, }); // Graceful shutdown — close-with-grace handles SIGINT, SIGTERM, // uncaught exceptions, and unhandled rejections automatically closeWithGrace({ delay: 10_000 }, async ({ signal, err }) => { if (err) { server.log.error({ err }, "server closing with error"); } else { server.log.info(`${signal} received, server closing`); } await server.close(); }); return server; } ``` -------------------------------- ### Configure Zod Compilers in a Fastify Plugin Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/schema-validation-zod.md Encapsulate the Zod validator and serializer compiler setup within a Fastify plugin for better organization in large applications. ```typescript import fp from "fastify-plugin"; import { serializerCompiler, validatorCompiler, } from "fastify-type-provider-zod"; export default fp( async function zodPlugin(fastify) { fastify.setValidatorCompiler(validatorCompiler); fastify.setSerializerCompiler(serializerCompiler); }, { name: "zod-plugin", }, ); ``` -------------------------------- ### Correct Fastify Implementation Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/_template.md Demonstrates the recommended way to implement a feature for optimal performance. This approach should be preferred. ```typescript // Good code example here const good = example(); ``` -------------------------------- ### Route Best Practices: Async Handlers and Full Options Source: https://context7.com/thecodepace/fastify-skills/llms.txt Organize routes as encapsulated plugins with folder-based prefixes. Always use `async` handlers and return values directly instead of using `reply.send()`. For complex routes, utilize the full `server.route()` options object, including schema definitions and pre-handlers. ```typescript // src/routes/users/index.ts import { FastifyInstance } from "fastify"; async function userRoutes(fastify: FastifyInstance) { // Prefer async + return over reply.send() fastify.get("/", async (request, reply) => { return fastify.db.query("SELECT * FROM users"); }); // Full route options for complex routes fastify.route({ method: "GET", url: "/:id", schema: { params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, response: { 200: { type: "object", properties: { id: { type: "string" }, name: { type: "string" } } } }, }, preHandler: [async (request, reply) => { /* auth check */ }], handler: async (request, reply) => { const { id } = request.params as { id: string }; return getUserById(id); }, }); } export default userRoutes; ``` -------------------------------- ### Creating a Zip Package for a Fastify Skill Source: https://github.com/thecodepace/fastify-skills/blob/main/AGENTS.md Command to create a zip archive of a Fastify skill for distribution, ensuring it matches the required naming conventions. ```bash cd skills zip -r {skill-name}.zip {skill-name}/ ``` -------------------------------- ### Fastify Route Structure with Parameterized Routes Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/autoload.md Organize routes using a directory structure where folders prefixed with `_` define route parameters. This allows for dynamic route matching like `/users/:id`. ```directory src/ routes/ users/ index.ts # GET /users → list all users _id/ index.ts # GET /users/:id → get user by id posts/ index.ts # GET /users/:id/posts → get posts for a user posts/ index.ts # GET /posts → list all posts _id/ index.ts # GET /posts/:id → get post by id comments/ index.ts # GET /posts/:id/comments → comments for a post _commentId/ index.ts # GET /posts/:id/comments/:commentId → single comment ``` -------------------------------- ### Organize User Routes with Plugin and Prefix Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/route-best-practices.md Organize user-related routes into a separate plugin file. This promotes modularity and allows for easy prefixing, such as '/v1/users'. ```typescript import { FastifyInstance } from "fastify"; async function userRoutes(fastify: FastifyInstance) { fastify.get("/", async (request, reply) => { // list users }); fastify.get("/:id", async (request, reply) => { // get user }); fastify.post("/", async (request, reply) => { // create user }); } export default userRoutes; ``` -------------------------------- ### Decorating the Reply Object with a Helper Method Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/decorators.md Add custom methods to the `Reply` object using `fastify.decorateReply()`. This example adds a `sendError` method for consistent error responses. ```typescript fastify.decorateReply("sendError", function (this: FastifyReply, statusCode: number, message: string) { return this.status(statusCode).send({ error: message }); }); fastify.get("/protected", async (request, reply) => { if (!request.headers.authorization) { return reply.sendError(401, "Unauthorized"); } return { ok: true }; }); ``` -------------------------------- ### Directory Structure for Fastify Skills Source: https://github.com/thecodepace/fastify-skills/blob/main/AGENTS.md Defines the standard directory structure for creating new Fastify skills, including the placement of skill definitions, scripts, and packaged distributions. ```bash skills/ {skill-name}/ # kebab-case directory name SKILL.md # Required: skill definition scripts/ # Required: executable scripts {script-name}.sh # Bash scripts (preferred) {skill-name}.zip # Required: packaged for distribution ``` -------------------------------- ### Fastify Application Hook: onClose for Cleanup Source: https://context7.com/thecodepace/fastify-skills/llms.txt Use the 'onClose' hook to perform cleanup tasks when the Fastify server instance is closed. This example demonstrates closing a database connection. ```typescript fastify.addHook("onClose", async (instance) => { await instance.db.end(); }); ``` -------------------------------- ### Compress or Modify Serialized Payload with onSend Hook Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/hooks-lifecycle.md This `onSend` hook example demonstrates how to compress a payload if it's a string longer than 1024 characters or perform other modifications before sending. ```typescript fastify.addHook("onSend", async (request, reply, payload) => { if (typeof payload === "string" && payload.length > 1024) { reply.header("Content-Encoding", "gzip"); return gzipPayload(payload); } return payload; }); ``` -------------------------------- ### Organize Post Routes with Plugin and Prefix Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/route-best-practices.md Organize post-related routes into a separate plugin file. This enhances modularity and supports prefixing, like '/v1/posts'. ```typescript import { FastifyInstance } from "fastify"; async function postRoutes(fastify: FastifyInstance) { fastify.get("/", async (request, reply) => { // list posts }); fastify.get("/:id", async (request, reply) => { // get post }); } export default postRoutes; ``` -------------------------------- ### Fastify Application Hook: onReady for Startup Checks Source: https://context7.com/thecodepace/fastify-skills/llms.txt The 'onReady' hook runs after the server is ready to accept connections. It's suitable for critical startup checks, like verifying database connectivity, and can abort startup if checks fail. ```typescript fastify.addHook("onReady", async () => { const healthy = await checkDatabaseConnection(); if (!healthy) throw new Error("Database unreachable — aborting startup"); }); ``` -------------------------------- ### Correctly Listen with Environment Configuration Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/configuration.md Use parsed environment variables for host and port when calling `server.listen` to ensure compatibility with deployment environments like Docker. This avoids hardcoding values and respects runtime configurations. ```typescript import { envSchema } from "./schema/env.js"; const config = envSchema.parse(process.env); const server = buildServer({ config }); await server.listen({ port: config.PORT, host: config.HOST, }); ``` -------------------------------- ### Incorrect: Direct Functionality in Server Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/create-plugin.md Avoid adding functionality directly to the Fastify server instance. This approach leads to less maintainable code. This example shows the incorrect pattern. ```typescript import Fastify from "fastify"; const server = Fastify(); server.decorate("utilityFunction", { foo: () => "bar", }); server.get("/", async (_, reply) => { return server.utilityFunction.foo(); }); await server.listen(3000); ``` -------------------------------- ### Autoload Plugins and Routes with @fastify/autoload Source: https://context7.com/thecodepace/fastify-skills/llms.txt Use `@fastify/autoload` to automatically register plugins and routes by scanning filesystem directories. Folder names map to URL prefixes, `_id` folders become `:id` parameters, and `_hooks.ts` files apply hooks scoped to their directory. Configure options like `autoHooks`, `cascadeHooks`, and `routeParams` for advanced behavior. ```typescript // npm install @fastify/autoload // src/server.ts import autoload from "@fastify/autoload"; import { join } from "node:path"; server.register(autoload, { dir: join(__dirname, "plugins") }); // shared services server.register(autoload, { dir: join(__dirname, "routes"), options: { prefix: "/api" }, autoHooks: true, // load _hooks.ts per directory cascadeHooks: true, // propagate parent hooks to children routeParams: true, // _id folder → :id param }); // Directory layout → generated URL prefixes: // routes/users/index.ts → GET/POST /api/users // routes/users/_id/index.ts → GET/PUT/DELETE /api/users/:id // routes/users/_id/posts/index.ts → GET /api/users/:id/posts ``` ```typescript // src/routes/users/_hooks.ts — auth applied to /api/users/* only import { FastifyInstance } from "fastify"; export default async function userHooks(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { await fastify.authenticate(request, reply); }); } ``` -------------------------------- ### Fastify Lifecycle Hook: onSend to Set Response Headers Source: https://context7.com/thecodepace/fastify-skills/llms.txt Utilize the 'onSend' hook to inject custom response headers after serialization but before the response is sent. This example adds a 'X-Request-Id' header. ```typescript fastify.addHook("onSend", async (request, reply, payload) => { reply.header("X-Request-Id", request.id); return payload; }); ``` -------------------------------- ### Build Fastify Server Factory with Autoload Source: https://context7.com/thecodepace/fastify-skills/llms.txt Implement a `buildServer()` factory function that accepts options and uses `@fastify/autoload` to load plugins and routes. This pattern enhances server reusability across different environments (development, production, tests) without requiring real ports. ```typescript // src/server.ts import path from "node:path"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import autoload from "@fastify/autoload"; import closeWithGrace from "close-with-grace"; import type { Env } from "./schema/env.js"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); export interface BuildServerOptions { config: Env; trustProxy?: boolean | string | number; } export function buildServer({ config, trustProxy }: BuildServerOptions) { const envToLogger = { development: { level: "debug", transport: { target: "pino-pretty", options: { translateTime: "HH:MM:ss Z", ignore: "pid,hostname" } } }, production: { level: config.LOG_LEVEL }, test: { level: "silent" }, } as const; const server = Fastify({ logger: envToLogger[config.NODE_ENV], trustProxy: trustProxy ?? false, requestTimeout: 120_000, bodyLimit: 1_048_576, return503OnClosing: true, forceCloseConnections: "idle", }); server.register(autoload, { dir: path.join(__dirname, "plugins") }); server.register(autoload, { dir: path.join(__dirname, "routes"), autoHooks: true, cascadeHooks: true }); closeWithGrace({ delay: 10_000 }, async ({ signal, err }) => { if (err) server.log.error({ err }, "server closing with error"); else server.log.info(`${signal} received, server closing`); await server.close(); }); return server; } // src/app.ts import { envSchema } from "./schema/env.js"; import { buildServer } from "./server.js"; const config = envSchema.parse(process.env); const server = buildServer({ config }); await server.listen({ port: config.PORT, host: config.HOST }); ``` -------------------------------- ### Build Fastify Server with Autoload Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/encapsulation.md Configures a Fastify server to automatically load plugins from the `plugins/` directory and routes from the `routes/` directory using @fastify/autoload. Shared plugins are registered with `fastify-plugin`, while routes maintain encapsulation. ```typescript import path from "node:path"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import autoload from "@fastify/autoload"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); export function buildServer(options = {}) { const server = Fastify(options); // Shared plugins — fastify-plugin breaks encapsulation intentionally server.register(autoload, { dir: path.join(__dirname, "plugins"), }); // Routes — stay encapsulated, prefixes from folder names server.register(autoload, { dir: path.join(__dirname, "routes"), autoHooks: true, cascadeHooks: true, }); return server; } ``` -------------------------------- ### Configuring @fastify/multipart with Limits and Validation Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/content-type-parser.md Use the `@fastify/multipart` plugin with explicit limits for file size, field size, and number of files/fields. Set `throwFileSizeLimit: true` to reject oversized files. ```typescript import fastifyMultipart from "@fastify/multipart"; app.register(fastifyMultipart, { limits: { fieldNameSize: 100, fieldSize: 1024 * 1024, // 1 MB fields: 10, fileSize: 10 * 1024 * 1024, // 10 MB files: 5, headerPairs: 2000, parts: 1000, }, throwFileSizeLimit: true, }); app.post("/upload", async (request, reply) => { const data = await request.file(); if (!data) { return reply.code(400).send({ error: "No file uploaded" }); } const buffer = await data.toBuffer(); return { filename: data.filename, mimetype: data.mimetype, size: buffer.length, }; }); ``` -------------------------------- ### Auto-attach Auth Hook to Routes with onRoute Source: https://github.com/thecodepace/fastify-skills/blob/main/skills/fastify-best-practise/rules/hooks-lifecycle.md The `onRoute` hook runs synchronously whenever a route is registered. This example demonstrates how to automatically attach an authentication hook to routes configured with `auth: true`. ```typescript fastify.addHook("onRoute", (routeOptions) => { if (routeOptions.config?.auth === true) { const existing = routeOptions.preHandler ?? []; routeOptions.preHandler = [ ...(Array.isArray(existing) ? existing : [existing]), authHook, ]; } }); ```