fastify
Fastify is a Node.js web framework built around one idea: JSON APIs get dramatically faster when you compile route schemas ahead of time. You attach JSON Schema to routes and Fastify compiles validation and serialization into generated functions, which is how it reaches its claimed 76k+ requests per second. Around that core sits a plugin architecture with encapsulation (plugins get isolated scopes for decorators and hooks), structured logging via Pino built in, and a large set of official @fastify/* plugins for CORS, JWT, rate limiting, Swagger, and more. It is inspired by Hapi and Express and is the most credible drop-in successor to Express for API servers.
The best default for plain Node JSON APIs today: fast, actively maintained, with an official plugin suite that covers the usual production needs. The schema-first and plugin-encapsulation models are the price of admission; if your team will not pay it, Express or Hono will serve you with less friction.
Use it if
- You are building a JSON API where throughput and latency actually matter and you want validation and serialization compiled from JSON Schema rather than bolted on
- You want structured logging from day one; Pino is wired in and enabled with a single logger: true option
- You want an officially maintained plugin suite (@fastify/cors, @fastify/jwt, @fastify/swagger, @fastify/rate-limit) instead of assembling third-party middleware of varying quality
- You are outgrowing Express and want a maintained framework with a similar mental model, first-class async/await, and real TypeScript support
- Your bottleneck is the database or an upstream API, which it usually is; framework throughput differences vanish in production numbers and Express familiarity may be worth more than benchmark wins
- You deploy to edge runtimes or want one codebase across Cloudflare Workers, Deno, and Bun; Hono is designed for that, Fastify is Node-first
- Your team will not invest in the plugin encapsulation model; register scopes, decorators, and fastify-plugin have a real learning curve, and misusing them produces confusing 'decorator not found' and ordering bugs
- You want a full application framework with dependency injection, modules, and conventions; NestJS provides that layer (and can even run Fastify underneath)
- You rely on a specific Express middleware ecosystem; many middlewares need @fastify/express or @fastify/middie shims, and some do not translate at all
Setup reality
npm i fastify installs clean with 15 direct dependencies (pino, find-my-way, fast-json-stringify, avvio, and friends), and npm init fastify scaffolds a project. The honest costs: the async plugin boot sequence (avvio) means registration order and missing awaits cause errors that surface far from their cause; getting real value out of Fastify means writing JSON Schema for routes, which is boilerplate until you adopt @sinclair/typebox or fastify-type-provider-zod; and .listen binds to 127.0.0.1 by default, so every first Docker deployment fails until you pass host: '0.0.0.0', a gotcha called out in the README itself. Major versions also arrive on a cadence (v5 is current, v4 maintained on a branch) with genuine migration work.
Patterns
Start a basic serverhello-server
import Fastify from 'fastify'
const fastify = Fastify({ logger: true })
fastify.get('/', async (request, reply) => {
return { hello: 'world' }
})
try {
await fastify.listen({ port: 3000 })
} catch (err) {
fastify.log.error(err)
process.exit(1)
}Returning an object from an async handler sends JSON automatically; no reply.send needed.
Validate a request body with JSON Schemaroute-validation
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' }
}
}
}
}, async (request) => {
// request.body is already validated here
return { created: request.body.name }
})Invalid bodies get an automatic 400 before your handler runs; format: 'email' needs ajv-formats registered in some setups.
Speed up responses with a response schemaresponse-serialization
fastify.get('/users/:id', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' }
}
}
}
}
}, async (request) => {
return { id: 1, name: 'ada', passwordHash: 'x' }
})Serialization strips fields not in the schema (passwordHash never leaves the server), which is both a speed and a safety feature; it also silently drops fields you forgot to declare.
Read path and query parametersurl-params
fastify.get('/items/:id', async (request) => {
const { id } = request.params
const { verbose } = request.query
return { id, verbose: verbose === 'true' }
})params and query values are strings unless a schema coerces them; declare types in schema.params to get numbers.
Share a decorator across the app with fastify-pluginplugin-encapsulation
import fp from 'fastify-plugin'
async function dbPlugin (fastify, opts) {
const db = await connect(opts.url)
fastify.decorate('db', db)
fastify.addHook('onClose', () => db.close())
}
export default fp(dbPlugin)
// app.js
await fastify.register(import('./db-plugin.js'), { url: process.env.DB_URL })
fastify.get('/ping', async function () {
return this.db.ping()
})Without the fp() wrapper the decorator stays private to the plugin's scope; that is the single most common Fastify confusion.
Protect routes with an onRequest hookauth-hook
fastify.register(async (protectedScope) => {
protectedScope.addHook('onRequest', async (request, reply) => {
const token = request.headers.authorization
if (!token || !(await verify(token))) {
return reply.code(401).send({ error: 'unauthorized' })
}
})
protectedScope.get('/admin', async () => ({ secret: true }))
})Hooks added inside a registered plugin apply only to routes in that scope, which is how you keep public routes public.
Set a custom error handlererror-handler
fastify.setErrorHandler((error, request, reply) => {
request.log.error(error)
if (error.validation) {
return reply.status(400).send({ error: 'bad request', details: error.validation })
}
reply.status(error.statusCode ?? 500).send({ error: 'internal error' })
})Validation failures arrive here too (error.validation is set); without a handler they leak schema details to clients.
Test routes without opening a porttesting-inject
import { test } from 'node:test'
import assert from 'node:assert'
import { build } from './app.js' // exports a configured Fastify instance
test('GET / returns hello', async () => {
const app = await build()
const res = await app.inject({ method: 'GET', url: '/' })
assert.strictEqual(res.statusCode, 200)
assert.deepStrictEqual(res.json(), { hello: 'world' })
await app.close()
})inject (from light-my-request) simulates HTTP in-process, so tests run fast and never fight over ports.
Listen correctly inside a containerdocker-listen
// Fastify binds to 127.0.0.1 by default, invisible from outside a container
await fastify.listen({ port: 3000, host: '0.0.0.0' })The README warns about this directly; bind 0.0.0.0 in containers, but never expose such a port on an untrusted network without a firewall in front.
Enable CORS with the official pluginregister-cors
import cors from '@fastify/cors'
await fastify.register(cors, {
origin: ['https://app.example.com'],
methods: ['GET', 'POST']
})Match the plugin's major to your Fastify major; @fastify/* plugins release new majors alongside each Fastify major.
Close cleanly on SIGTERMgraceful-shutdown
process.on('SIGTERM', async () => {
try {
await fastify.close() // runs onClose hooks, stops accepting connections
process.exit(0)
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
})fastify.close() triggers every plugin's onClose hook, so database pools registered via plugins shut down with the server.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| express | npm | You want the largest middleware ecosystem and tutorial coverage and throughput is not a concern |
| hono | npm | You need one framework across Node, Bun, Deno, and edge runtimes like Cloudflare Workers |
| koa | npm | You want a minimal middleware kernel and prefer picking every other piece yourself |
| @nestjs/core | npm | You want an opinionated application architecture with DI and decorators on top of the HTTP layer |