Fastify 5 Best Practices
Table of Contents
Request lifecycle (exact order)
Incoming Request
└─ Routing
└─ onRequest hooks
└─ preParsing hooks
└─ Content-Type Parsing
└─ preValidation hooks
└─ Schema Validation (→ 400 on failure)
└─ preHandler hooks
└─ Route Handler
└─ preSerialization hooks
└─ onSend hooks
└─ Response Sent
└─ onResponse hooks
Error at any stage → onError hooks → error handler → onSend → response → onResponse.
Top anti-patterns
Mixing async/callback in handlers — Use async OR callbacks, never both. With async, return the value; don't call reply.send() AND return.
Returning undefined from async handler — Fastify treats this as "no response yet". Return the data or call reply.send().
Using arrow functions when you need this — Arrow functions don't bind this to the Fastify instance. Use function declarations for handlers that need this.
Forgetting fastify-plugin wrapper — Without it, decorators/hooks stay scoped to the child context. Parent and sibling plugins won't see them.
Decorating with reference types directly — decorateRequest('data', {}) shares the SAME object across all requests. Use null initial + onRequest hook to assign per-request.
Sending response in onError hook — onError is read-only for logging. Use setErrorHandler() to modify error responses.
Not handling reply.send() in async hooks — Call return reply after reply.send() in async hooks to prevent "Reply already sent" errors.
Ignoring encapsulation — Decorators/hooks registered in child plugins are invisible to parents. Design your plugin tree carefully.
String concatenation in SQL from route params — Always use parameterized queries. Fastify validates input shape, not content safety.
Missing response schema — Without response schema, Fastify serializes with JSON.stringify() (slow) and may leak sensitive fields. Use fast-json-stringify via response schemas.
Quick patterns
Plugin with fastify-plugin (FastifyPluginCallback)
Project convention: use FastifyPluginCallback + done() (avoids require-await lint errors).
import fp from "fastify-plugin";
import type { FastifyPluginCallback } from "fastify";
const myPlugin: FastifyPluginCallback = (fastify, opts, done) => {
fastify.decorate("myService", new MyService());
done();
};
export default fp(myPlugin, { name: "my-plugin" });
Route with validation
fastify.post<{ Body: CreateUserBody }>("/users", {
schema: {
body: {
type: "object",
required: ["email", "name"],
properties: {
email: { type: "string", format: "email" },
name: { type: "string", minLength: 1 },
},
},
response: {
201: {
type: "object",
properties: {
id: { type: "string" },
email: { type: "string" },
},
},
},
},
handler: async (request, reply) => {
const user = await createUser(request.body);
return reply.code(201).send(user);
},
});
Hook (application-level)
fastify.addHook("onRequest", async (request, reply) => {
request.startTime = Date.now();
});
fastify.addHook("onResponse", async (request, reply) => {
request.log.info({ elapsed: Date.now() - request.startTime }, "request completed");
});
Error handler
fastify.setErrorHandler((error, request, reply) => {
request.log.error(error);
const statusCode = error.statusCode ?? 500;
reply.code(statusCode).send({
error: statusCode >= 500 ? "Internal Server Error" : error.message,
});
});
Reference files
Load the relevant file when you need detailed API information:
- Server factory & options — constructor options, server methods, properties: references/server-and-options.md
- Routes & handlers — declaration, URL params, async patterns, constraints: references/routes-and-handlers.md
- Hooks & lifecycle — all 16 hook types, signatures, scope, early response: references/hooks-and-lifecycle.md
- Plugins & encapsulation — creating plugins, fastify-plugin, context inheritance: references/plugins-and-encapsulation.md
- Validation & serialization — JSON Schema, Ajv, response schemas, custom validators: references/validation-and-serialization.md
- Request, Reply & errors — request/reply API, error handling, FST_ERR codes: references/request-reply-errors.md
- TypeScript & logging — route generics, type providers, Pino config, decorators: references/typescript-and-logging.md
1---2name: fastify-best-practices-23description: Fastify 5 best practices, API reference, and patterns for routes, plugins, hooks, validation, error handling, and TypeScript. Use when: (1) writing new Fastify routes, plugins, or hooks, (2) looking up Fastify API signatures or options, (3) debugging Fastify issues (lifecycle, encapsulation, validation, plugin timeout), (4) reviewing Fastify code for anti-patterns. Triggers: 'add a route', 'create plugin', 'Fastify hook', 'validation schema', 'Fastify error', 'setErrorHandler', 'fastify-plugin'.4---56# Fastify 5 Best Practices78## Table of Contents910- [Request lifecycle](#request-lifecycle-exact-order)11- [Top anti-patterns](#top-anti-patterns)12- [Quick patterns](#quick-patterns)13- [Reference files](#reference-files)1415<quick_reference>1617## Request lifecycle (exact order)1819```20Incoming Request21 └─ Routing22 └─ onRequest hooks23 └─ preParsing hooks24 └─ Content-Type Parsing25 └─ preValidation hooks26 └─ Schema Validation (→ 400 on failure)27 └─ preHandler hooks28 └─ Route Handler29 └─ preSerialization hooks30 └─ onSend hooks31 └─ Response Sent32 └─ onResponse hooks33```3435Error at any stage → `onError` hooks → error handler → `onSend` → response → `onResponse`.3637</quick_reference>3839<anti_patterns>4041## Top anti-patterns42431. **Mixing async/callback in handlers** — Use `async` OR callbacks, never both. With async, `return` the value; don't call `reply.send()` AND return.44452. **Returning `undefined` from async handler** — Fastify treats this as "no response yet". Return the data or call `reply.send()`.46473. **Using arrow functions when you need `this`** — Arrow functions don't bind `this` to the Fastify instance. Use `function` declarations for handlers that need `this`.48494. **Forgetting `fastify-plugin` wrapper** — Without it, decorators/hooks stay scoped to the child context. Parent and sibling plugins won't see them.50515. **Decorating with reference types directly** — `decorateRequest('data', {})` shares the SAME object across all requests. Use `null` initial + `onRequest` hook to assign per-request.52536. **Sending response in `onError` hook** — `onError` is read-only for logging. Use `setErrorHandler()` to modify error responses.54557. **Not handling `reply.send()` in async hooks** — Call `return reply` after `reply.send()` in async hooks to prevent "Reply already sent" errors.56578. **Ignoring encapsulation** — Decorators/hooks registered in child plugins are invisible to parents. Design your plugin tree carefully.58599. **String concatenation in SQL from route params** — Always use parameterized queries. Fastify validates input shape, not content safety.606110. **Missing response schema** — Without `response` schema, Fastify serializes with `JSON.stringify()` (slow) and may leak sensitive fields. Use `fast-json-stringify` via response schemas.6263</anti_patterns>6465<examples>6667## Quick patterns6869### Plugin with fastify-plugin (FastifyPluginCallback)7071Project convention: use `FastifyPluginCallback` + `done()` (avoids `require-await` lint errors).7273```ts74import fp from "fastify-plugin";75import type { FastifyPluginCallback } from "fastify";7677const myPlugin: FastifyPluginCallback = (fastify, opts, done) => {78 fastify.decorate("myService", new MyService());79 done();80};8182export default fp(myPlugin, { name: "my-plugin" });83```8485### Route with validation8687```ts88fastify.post<{ Body: CreateUserBody }>("/users", {89 schema: {90 body: {91 type: "object",92 required: ["email", "name"],93 properties: {94 email: { type: "string", format: "email" },95 name: { type: "string", minLength: 1 },96 },97 },98 response: {99 201: {100 type: "object",101 properties: {102 id: { type: "string" },103 email: { type: "string" },104 },105 },106 },107 },108 handler: async (request, reply) => {109 const user = await createUser(request.body);110 return reply.code(201).send(user);111 },112});113```114115### Hook (application-level)116117```ts118fastify.addHook("onRequest", async (request, reply) => {119 request.startTime = Date.now();120});121122fastify.addHook("onResponse", async (request, reply) => {123 request.log.info({ elapsed: Date.now() - request.startTime }, "request completed");124});125```126127### Error handler128129```ts130fastify.setErrorHandler((error, request, reply) => {131 request.log.error(error);132 const statusCode = error.statusCode ?? 500;133 reply.code(statusCode).send({134 error: statusCode >= 500 ? "Internal Server Error" : error.message,135 });136});137```138139</examples>140141<references>142143## Reference files144145Load the relevant file when you need detailed API information:146147- **Server factory & options** — constructor options, server methods, properties: [references/server-and-options.md](references/server-and-options.md)148- **Routes & handlers** — declaration, URL params, async patterns, constraints: [references/routes-and-handlers.md](references/routes-and-handlers.md)149- **Hooks & lifecycle** — all 16 hook types, signatures, scope, early response: [references/hooks-and-lifecycle.md](references/hooks-and-lifecycle.md)150- **Plugins & encapsulation** — creating plugins, fastify-plugin, context inheritance: [references/plugins-and-encapsulation.md](references/plugins-and-encapsulation.md)151- **Validation & serialization** — JSON Schema, Ajv, response schemas, custom validators: [references/validation-and-serialization.md](references/validation-and-serialization.md)152- **Request, Reply & errors** — request/reply API, error handling, FST_ERR codes: [references/request-reply-errors.md](references/request-reply-errors.md)153- **TypeScript & logging** — route generics, type providers, Pino config, decorators: [references/typescript-and-logging.md](references/typescript-and-logging.md)154155</references>