cheat sheet
fastify
Package-level reference for the Fastify framework on npm — install, plugin model, Node support, and validation/encapsulation gotchas.
fastify
What it is
fastify is a Node.js web framework focused on raw throughput, schema-driven validation, and an encapsulation-aware plugin system. Routes declare their request/response shapes via JSON Schema, and Fastify uses that schema to validate inputs and serialize responses with a pre-compiled fast-json-stringify function — the source of most of its performance edge over Express.
Reach for fastify when you need a typed, validated, observable Node HTTP server with a sane plugin model. Reach for hono if you target edge runtimes (Cloudflare Workers, Deno Deploy, Bun), express for the simplest possible middleware stack, or koa / h3 for thinner cores.
Install
Fastify is a library, used inside your own application bin or via a process manager.
npm install fastify
Output: added fastify to dependencies
pnpm add fastify
Output: added 1 package, linked from store
yarn add fastify
Output: added fastify
bun add fastify
Output: installed fastify
deno add npm:fastify
Output: added npm:fastify to import map (Node-compat required)
Versioning & Node support
Current line is fastify@5.x (released late 2024).
fastify@5— Node 20+ LTS only. Dual ESM/CJS.fastify@4— Node 14.21+ / 16.x / 18.x. Still receives security patches.- Strict semver — major bumps deprecate APIs at the previous major and remove them at the next. The 4 → 5 migration removed several
reply.send()edge cases and tightened types. - TypeScript types ship in-tree and are first-class — Fastify's TS contributor team is unusually responsive.
Package metadata
- Maintainer: Matteo Collina + Fastify contributors (
fastifyorg on GitHub) - Project home: github.com/fastify/fastify
- Docs: fastify.dev
- npm: npmjs.com/package/fastify
- License: MIT
- First released: 2017
- Downloads: ~3 million+ weekly downloads — the most popular high-performance Node framework after Express.
Peer dependencies & extras
Fastify core has zero peer-deps but expects you to install plugins from the @fastify/* org as needed. There is no extras flag — each plugin is its own npm package.
Commonly-paired plugins:
@fastify/cors— CORS handling@fastify/helmet— secure headers@fastify/jwt— JWT auth + verification@fastify/cookie,@fastify/session— cookie / session middleware@fastify/rate-limit— request throttling@fastify/static— serve static files@fastify/multipart— multipart/form-data@fastify/websocket— WebSocket viaws@fastify/swagger+@fastify/swagger-ui— OpenAPI generation from route schemas@fastify/under-pressure— load shedding under memory pressure@fastify/oauth2,@fastify/passport— auth integrations
Companion ecosystem:
pino— Fastify's default logger. Already pulled in transitively; configure viaapp.log.ajv— JSON Schema validator. Also transitive; customise viaajvOptions.fast-json-stringify— response serializer. Transitive.@platformatic/db— higher-level platform built on Fastify
Alternatives
| Framework | Trade-off |
|---|---|
express | Oldest and most-known. Slower, no built-in validation, weaker types. Massive middleware ecosystem. |
hono | Edge-first; runs on Workers / Deno / Bun / Node. Smaller core, similar speed. Best when targeting non-Node runtimes. |
koa | Express creators' modern rewrite. Smaller core, async/await native. Less ecosystem momentum. |
h3 | Used by Nuxt / Nitro. Edge-friendly. Pairs with Nitro for full deploy adapter coverage. |
nestjs | Class+decorator framework that can run on top of Fastify. Heavier, opinionated DI. |
restify | Older REST-focused framework. Largely superseded by Fastify. |
Common gotchas
- Schema-first validation requires real JSON Schema. Passing a TypeScript interface or Zod schema directly does NOT work. Use
@sinclair/typebox(recommended),zod-to-json-schema, or write JSON Schema manually. Without aresponseschema the serializer falls back to slowJSON.stringify. - Plugin encapsulation creates request-decorator scope issues. Decorators registered inside an
app.register(plugin)are visible only within that plugin's encapsulated scope. To share a decorator app-wide, wrap withfastify-plugin(the helper) to break encapsulation deliberately. - Logging defaults to Pino in production mode. Without
pino-prettyconfigured, logs are NDJSON — useful in production, painful locally. Configure:logger: { transport: { target: "pino-pretty" } }in dev only. reply.send()is mostly fire-and-forget. Callingawait reply.send(data)is rarely what you want — Fastify already returns the value if youreturn datafrom the handler. Mixingreply.send+returncauses double-send errors.- Hooks vs middleware vs plugins. Three overlapping concepts:
onRequest/preHandler/onSendhooks (lifecycle),app.addHookvsapp.register(plugin). Middleware (app.use) is Express-compat and discouraged for new code. - Validation errors return 400 by default. Customise via
setErrorHandlerto return your preferred error envelope. Without customisation the response shape may not match your API contract. reply.rawbypasses Fastify entirely. Writing toreply.raw(the underlying Node socket) skips serialization, lifecycle hooks, and content-type negotiation. Use only for streaming SSE or websocket upgrades.- TypeScript route generics are verbose. The full
RouteGenericInterface(Body,Querystring,Params,Headers,Reply) is loud. UseTypeBoxTypeProvideror theJsonSchemaToTsProviderplugins to infer types from your schemas instead of duplicating. - Production listener:
host: '0.0.0.0', not localhost. The default bind is127.0.0.1, which won't accept external connections inside Docker / Kubernetes. Always sethost: '0.0.0.0'for containerised deploys.
Real-world recipes
Schema-validated route with TypeBox
TypeBox produces JSON Schema at module load time and feeds it directly to Fastify's validator + serializer. The result: full typed inference end-to-end without code generation.
import Fastify from "fastify";
import { Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
const app = Fastify({ logger: true }).withTypeProvider<TypeBoxTypeProvider>();
const CreateUser = Type.Object({
email: Type.String({ format: "email" }),
age: Type.Optional(Type.Integer({ minimum: 0 })),
});
const UserResp = Type.Object({ id: Type.String(), email: Type.String() });
app.post("/users", {
schema: { body: CreateUser, response: { 201: UserResp } },
handler: async (req, reply) => {
// req.body is fully typed: { email: string; age?: number }
const id = crypto.randomUUID();
reply.code(201);
return { id, email: req.body.email };
},
});
await app.listen({ port: 3000, host: "0.0.0.0" });
Output: valid request returns 201 + JSON; missing email returns 400 with structured error.
JWT auth via @fastify/jwt
Most APIs need an auth layer. @fastify/jwt handles signing, verification, and request-decoration.
import Fastify from "fastify";
import jwt from "@fastify/jwt";
const app = Fastify();
await app.register(jwt, { secret: process.env.JWT_SECRET! });
app.decorate("authenticate", async (req, reply) => {
try {
await req.jwtVerify();
} catch {
reply.code(401).send({ error: "Unauthorized" });
}
});
app.post("/login", async (req) => {
// ...validate credentials...
return { token: app.jwt.sign({ sub: "user-id" }) };
});
app.get("/me", { preHandler: app.authenticate }, async (req) => {
return { user: req.user };
});
await app.listen({ port: 3000, host: "0.0.0.0" });
Output: /login returns a JWT; /me returns 401 without it, decoded user with it.
WebSocket route
@fastify/websocket adds the wsHandler option to routes.
import Fastify from "fastify";
import websocket from "@fastify/websocket";
const app = Fastify();
await app.register(websocket);
app.get("/chat", { websocket: true }, (socket, req) => {
socket.on("message", (msg) => {
socket.send(`echo: ${msg.toString()}`);
});
});
await app.listen({ port: 3000, host: "0.0.0.0" });
Output: client connecting to ws://localhost:3000/chat receives echo: <message> for each message.
File upload with @fastify/multipart
import Fastify from "fastify";
import multipart from "@fastify/multipart";
import { pipeline } from "node:stream/promises";
import { createWriteStream } from "node:fs";
const app = Fastify();
await app.register(multipart, { limits: { fileSize: 10 * 1024 * 1024 } });
app.post("/upload", async (req, reply) => {
const data = await req.file();
if (!data) return reply.code(400).send({ error: "no file" });
await pipeline(data.file, createWriteStream(`/tmp/${data.filename}`));
return { saved: data.filename, size: data.file.bytesRead };
});
await app.listen({ port: 3000, host: "0.0.0.0" });
Output: uploaded file streamed to disk; multipart parsing happens on demand, so a malicious client can't consume memory by uploading 10 GB.
Graceful shutdown
Production servers need to drain in-flight requests before exiting. Fastify has close(); pair with signal handlers.
import Fastify from "fastify";
const app = Fastify();
app.get("/", async () => ({ ok: true }));
await app.listen({ port: 3000, host: "0.0.0.0" });
for (const sig of ["SIGTERM", "SIGINT"] as const) {
process.on(sig, async () => {
app.log.info(`received ${sig}, draining…`);
await app.close();
process.exit(0);
});
}
Output: existing connections complete; new ones get connection-refused; clean exit.
Production deployment
Fastify is a long-lived Node process. The deployment concerns are process management, clustering, observability, and graceful restarts.
Process manager: PM2
npm install -g pm2
pm2 start dist/server.js -i max --name api
pm2 startup # generate the systemd unit
pm2 save
Output: runs N workers (one per CPU core) under PM2, restarts on crash, auto-starts on boot.
-i max enables Node cluster mode — multiple workers share the same port via the OS load balancer. For CPU-bound work this scales linearly with cores.
Clustering with node:cluster
For full control without PM2:
import cluster from "node:cluster";
import os from "node:os";
if (cluster.isPrimary) {
for (let i = 0; i < os.availableParallelism(); i++) cluster.fork();
cluster.on("exit", (worker) => {
console.log(`worker ${worker.process.pid} exited; restarting`);
cluster.fork();
});
} else {
await import("./server.js");
}
Output: primary forks one worker per core; each worker runs Fastify.
For most workloads PM2 (or a similar tool like forever) is simpler than rolling your own cluster.
Behind a reverse proxy
Run Fastify behind Nginx, Caddy, HAProxy, or a cloud load balancer. The proxy handles TLS termination, HTTP/2, and static assets.
# /etc/nginx/sites-available/api
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Enable trustProxy: true in Fastify so it honours X-Forwarded-* headers:
const app = Fastify({ trustProxy: true });
Output: req.ip resolves the real client IP, not the proxy's loopback.
Containerised deploy
FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-slim
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY dist ./dist
COPY package.json .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
Bind to 0.0.0.0 inside containers — the default 127.0.0.1 is unreachable from outside.
Health checks
Most platforms expect a /health or /healthz endpoint:
app.get("/healthz", { logLevel: "warn" }, async () => ({ ok: true }));
Use logLevel: "warn" so health-check polls don't flood the log.
Performance tuning
Fastify's main performance edge over Express is schema-driven serialization. Use it.
- Always declare response schemas. Without a
responseschema, Fastify calls plainJSON.stringify. With one, it usesfast-json-stringify— 2-5× faster for typical payloads. - Tune the logger. Pino is fast but logging still has cost. Use
level: "info"in production,level: "warn"for static health checks. Avoidpino-prettyoutside dev. - Reuse schema objects. Inline schemas force AJV to re-compile on every route registration. Hoist to constants or use
addSchemato share by$id. - Avoid
JSON.parse(body)in handlers. Fastify already parsesapplication/json; double-parsing wastes CPU. - Stream large responses. For >1 MB JSON, stream from a generator or use
reply.rawdirectly. Pre-buffered responses dominate memory under load. - Plugin encapsulation has a cost. Each
registerboundary adds setup overhead. For very hot paths, register the plugin once at root rather than per-context. - HTTP/2 vs HTTP/1. Fastify supports HTTP/2 via the
http2: trueoption. For most APIs the reverse proxy speaks HTTP/2 externally and HTTP/1 internally — keep Fastify on HTTP/1. - Benchmark with
autocannon. It's the tool Fastify's maintainers use.npx autocannon -c 100 -d 10 http://localhost:3000/.
A realistic baseline: a simple /users/:id route with TypeBox validation + schema serialization handles ~30,000 req/s per core on a modern x86 chip (Node 22). Plain Express hits ~10,000.
Version migration guide
| From | To | Notable changes |
|---|---|---|
fastify@3 | fastify@4 | Major TypeScript types overhaul. reply.send return type changed. Several deprecated APIs removed. |
fastify@4 | fastify@5 | Node 20+ floor. Querystring parser switched to node:url. Several reply edge cases tightened (no more silent double-send). Plugins must declare correct fastify peer-dep range. |
5.x migration checklist:
- Bump Node engine to 20+.
- Re-check every
app.register— plugin signatures changed slightly to accept typedFastifyPluginCallback/FastifyPluginAsync. - Audit
reply.send(...)calls — mixing withreturn datanow errors instead of silently overwriting. - Update
@fastify/*plugins; they pin a major peer-dep range. - Run
tsc --noEmit— most breaking changes are caught at compile. - Verify schemas still validate after the AJV bump (AJV 8.x is stricter on draft-07 vs 2019-09 mixing).
For long-term stability, pin minor in package.json ("fastify": "5.x") and run npm outdated fastify monthly.
ESM/CJS interop & bundling
Fastify dual-publishes ESM and CJS. The interop notes are about how plugins and TypeScript projects consume it.
| Setup | Pattern |
|---|---|
| ESM Node | import Fastify from "fastify"; |
| CJS Node | const Fastify = require("fastify"); |
| TypeScript ESM | Same as ESM. Set "module": "Node16" (or NodeNext). |
| TypeScript CJS | Set "module": "commonjs". Some @fastify/* plugins are ESM-only — pin minor versions if you must stay on CJS. |
| Bundling | esbuild --bundle --platform=node --target=node20 works. Fastify's many plugins use dynamic require — pass --external for things that bundle poorly. |
| Bun | Mostly works. Some plugins use node:cluster or low-level streams that Bun handles differently — test before relying. |
| Deno | import Fastify from "npm:fastify". Node-compat is required. |
| Edge runtimes | Fastify is NOT suitable for Cloudflare Workers / Vercel Edge / Deno Deploy — it expects a long-lived Node process with the http module. Use hono or h3 instead. |
The @fastify/* plugins are ESM-first in their 4.x+ lines. CJS projects need to either preload via await import(...) or pin older plugin majors.
Plugin & ecosystem coverage
The @fastify/* org is the canonical plugin namespace. Third-party plugins use fastify-* (older) or any name with the right peer-dep.
| Plugin | Purpose |
|---|---|
@fastify/cors | CORS handling with preflight cache. |
@fastify/helmet | Secure HTTP headers. |
@fastify/jwt | JWT signing, verification, and request decoration. |
@fastify/cookie | Cookie parsing and signing. |
@fastify/session | Cookie-based sessions. |
@fastify/rate-limit | Token-bucket rate limiting with Redis backend support. |
@fastify/static | Serve static files (preferably let the reverse proxy do this). |
@fastify/multipart | multipart/form-data parsing. |
@fastify/websocket | WebSocket via ws. |
@fastify/swagger + @fastify/swagger-ui | OpenAPI generation from route schemas. |
@fastify/sensible | Convenience methods (reply.notFound(), reply.badRequest()). |
@fastify/under-pressure | Load shedding when event-loop lag or RSS exceed thresholds. |
@fastify/redis | Redis client wired into the app instance. |
@fastify/postgres | Postgres pool. |
@fastify/mysql | MySQL pool. |
@fastify/mongodb | MongoDB client. |
@fastify/etag | Auto-generates ETag headers. |
@fastify/compress | gzip / brotli response compression. |
@fastify/circuit-breaker | Circuit-breaker pattern for upstream calls. |
@fastify/type-provider-typebox | Type-provider wiring TypeBox schemas to route handlers. |
@fastify/type-provider-json-schema-to-ts | Alternative type provider for plain JSON Schema. |
fastify-plugin | Helper to declare a plugin that breaks encapsulation. |
mercurius | GraphQL adapter for Fastify. |
pino, pino-pretty, pino-roll | Logger ecosystem. |
Testing & CI integration
Fastify has first-class support for in-process testing via app.inject() — no port binding, no HTTP, full lifecycle.
inject() for unit tests
import { test, expect } from "vitest";
import Fastify from "fastify";
test("GET / returns ok", async () => {
const app = Fastify();
app.get("/", async () => ({ ok: true }));
const res = await app.inject({ method: "GET", url: "/" });
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual({ ok: true });
await app.close();
});
Output: 1 passed. No real TCP socket; the test runs in microseconds.
Suite-level helpers
Hoist app construction so each test starts clean:
import { beforeEach, afterEach } from "vitest";
import Fastify, { FastifyInstance } from "fastify";
let app: FastifyInstance;
beforeEach(async () => {
app = Fastify();
// ...register routes / plugins
await app.ready();
});
afterEach(async () => {
await app.close();
});
CI pipeline
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npm test
- run: npx autocannon -c 100 -d 5 http://localhost:3000/ # smoke perf check
if: github.event_name == 'pull_request'
Snapshot OpenAPI
When using @fastify/swagger, snapshot the generated OpenAPI to catch unintended API contract changes.
test("OpenAPI is stable", async () => {
const app = await buildApp();
await app.ready();
expect(app.swagger()).toMatchSnapshot();
await app.close();
});
Security considerations
- Always validate inputs via schema. A route without a body / querystring / params schema accepts anything. Schema validation IS your input layer.
- Use
@fastify/helmet. Sets sane secure headers (CSP, HSTS, X-Content-Type-Options, etc.) with one line:app.register(helmet). - Rate-limit auth endpoints.
@fastify/rate-limitwith stricter limits on/login,/signup,/reset-passwordprevents brute force. - Don't trust
req.headers["x-forwarded-*"]withouttrustProxy: true. Fastify rejects spoofed headers unless explicitly trusted. Configure with the number of proxies in front of you, not justtrue, to harden further. @fastify/jwtdefaults to HS256. For multi-service architectures, switch to RS256 or ES256 with public-key verification.- Don't log full request bodies in production. PII / passwords / tokens land in your log store. Configure Pino's
redactto mask sensitive paths. @fastify/multipartfile-size limits. Default is unlimited — setlimits: { fileSize: ... }or denial-of-service via large uploads is trivial.- Disable
app.usefor new code. Express-style middleware bypasses Fastify's lifecycle and validation. Use hooks instead. - Audit transitive deps. Fastify itself is well-audited; third-party
@fastify/*plugins have varying maintenance. Runnpm auditand pin minor versions of every plugin. reply.rawbypasses everything. Use only when you understand the consequences (SSE, custom binary protocols).
Troubleshooting common errors
FST_ERR_VALIDATION (400) — request failed schema validation. The response body details which field. Adjust the schema or fix the client payload.
FST_ERR_INSTANCE_ALREADY_LISTENING — calling app.listen() twice. Common in tests where the test suite shares an app. Use app.inject for tests instead.
Cannot set headers after they are sent — mixed reply.send(data) with return data. Pick one. Most idiomatic: return data from the handler.
FastifyError: Failed building the validation schema — schema isn't valid JSON Schema. Common cause: passing a Zod schema directly. Convert via zod-to-json-schema or use TypeBox.
Plugin's decorator not visible in route handler — encapsulation. The decorator was registered inside a sub-plugin's scope. Wrap the plugin with fastify-plugin to expose globally, or move the decorator registration up.
Slow JSON serialization — no response schema. Add one matching your payload shape; serialization speed jumps.
request.body is undefined — Content-Type doesn't match a parser (most often missing application/json). Either set the header client-side or call app.addContentTypeParser for your custom type.
EADDRINUSE — port already taken. Find with lsof -i :3000 and kill, or pass a different port.
Cannot find module 'pino-pretty' — pretty transport configured but not installed as a dependency. npm install -D pino-pretty and gate it behind NODE_ENV !== "production".
Connection: close on every response — keep-alive disabled or the client isn't sending one. Verify with curl -v — Fastify supports keep-alive by default.
When NOT to use this
Skip Fastify when:
- You target edge runtimes. Fastify needs Node's
httpmodule. For Workers / Vercel Edge / Deno Deploy, usehonoorh3. They share the JSON Schema validation idiom but run on Web standards. - The API is trivially small. A single
GET /healthendpoint is fine withhttp.createServer. Fastify's plugin model becomes overhead. - You want classical decorators / DI.
nestjs(which can run on Fastify under the hood) offers that model with more structure. Pure Fastify is functional, not OOP. - You're rewriting an Express app and don't have time to convert middleware. Express's middleware ecosystem has 15+ years of inertia. Fastify supports Express middleware via
@fastify/expressbut it's a compatibility shim, not a goal. - The runtime is Bun-only. Bun ships its own fast HTTP server (
Bun.serve). Fastify works on Bun but you forfeit Bun-native speed. - You write a pure GraphQL backend.
mercuriusis Fastify-native but Apollo Server has more docs. The Fastify edge isn't dramatic for a single GraphQL entry point.
See also
- JavaScript: fastify — routes, plugins, hooks, validation, testing
- Concept: http — request/response semantics
- Concept: json — JSON Schema validation & serialization