Fastify: High-Performance Web Framework
Fastify is a web framework for Node.js built around two ideas: serve requests as fast as possible, and make the developer experience pleasant while doing it. It reaches its speed through a highly optimized router, schema-driven JSON serialization, and a lightweight plugin architecture that encapsulates state instead of mutating shared globals. If you are building JSON APIs and want low overhead per request plus first-class validation, Fastify is a strong default in 2026.
Installing and a first server
Fastify ships as an ES module-friendly package and works with Node.js 20/22 LTS. Install it alongside nothing else — the core is self-contained.
npm install fastify
A minimal server registers routes and then calls listen. Fastify’s listen returns a promise, so you can await it and handle startup errors cleanly.
import Fastify from 'fastify';
const app = Fastify({ logger: true });
app.get('/health', async (request, reply) => {
return { status: 'ok', uptime: process.uptime() };
});
try {
await app.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
app.log.error(err);
process.exit(1);
}
Output:
{"level":30,"time":1718323200000,"msg":"Server listening at http://0.0.0.0:3000"}
Returning a value (or a resolved promise) from a handler sends it as the response. You only call
reply.send()explicitly when you need to set status codes or headers first. Never mixreturnandreply.send()in the same handler.
CommonJS users can swap the import for const Fastify = require('fastify'); the rest of the API is identical.
Routing
Fastify uses find-my-way, a radix-tree router that matches routes in roughly constant time regardless of how many you register. You can declare routes with shorthand methods (app.get, app.post, …) or the full app.route() form, which accepts schema, hooks, and the handler together.
app.route({
method: 'GET',
url: '/users/:id',
handler: async (request, reply) => {
const { id } = request.params;
return { id, name: 'Ada Lovelace' };
},
});
Route parameters live on request.params, query strings on request.query, and the parsed body on request.body. Fastify parses application/json and application/x-www-form-urlencoded out of the box.
JSON schema validation and serialization
This is Fastify’s signature feature. You attach a JSON Schema to a route, and Fastify uses it for two things: validating incoming requests (via Ajv) and serializing outgoing responses (via fast-json-stringify). Schema-based serialization is dramatically faster than JSON.stringify because Fastify compiles a purpose-built function for the exact shape you declared.
const createUserSchema = {
body: {
type: 'object',
required: ['email', 'age'],
properties: {
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 18 },
},
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
},
},
},
};
app.post('/users', { schema: createUserSchema }, async (request, reply) => {
const { email } = request.body;
reply.code(201);
return { id: crypto.randomUUID(), email };
});
If a request fails validation, Fastify returns a 400 automatically — your handler never runs.
Output:
$ curl -s -XPOST localhost:3000/users -H 'content-type: application/json' -d '{"email":"x","age":12}'
{"statusCode":400,"error":"Bad Request","message":"body/email must match format \"email\""}
Note that the response schema also acts as a filter: any property not declared is stripped before sending, which prevents accidental leakage of sensitive fields.
The plugin system
Everything in Fastify is a plugin, including your own application. A plugin is an async function that receives the instance and an options object. Plugins create an encapsulated context — decorators, hooks, and routes registered inside a plugin do not leak to its siblings, only to its children. This is what makes large Fastify apps stay organized.
async function userRoutes(instance, opts) {
instance.get('/users', async () => [{ id: '1' }]);
instance.post('/users', async (req) => req.body);
}
app.register(userRoutes, { prefix: '/api/v1' });
You extend instances, requests, and replies with decorate, decorateRequest, and decorateReply — a cheaper, monomorphic alternative to attaching properties at runtime.
app.decorate('db', await connectToDatabase());
app.get('/count', async (request) => {
return { total: await request.server.db.countUsers() };
});
Use
fastify-pluginto break encapsulation deliberately when a plugin (like a database connection) should be shared with the whole tree rather than scoped to its registration point.
Hooks
Hooks let you run logic at defined points in the request/response lifecycle — authentication, logging, response shaping. They run in registration order and respect plugin encapsulation.
| Hook | Fires |
|---|---|
onRequest | Earliest, before body parsing — ideal for auth |
preValidation | Before schema validation |
preHandler | After validation, before the handler |
onSend | Before the payload is sent, can mutate it |
onResponse | After the response is fully sent |
onError | When a handler or hook throws |
app.addHook('onRequest', async (request, reply) => {
if (!request.headers.authorization) {
reply.code(401).send({ error: 'Unauthorized' });
}
});
Why Fastify is faster than Express
Express works fine, but its design predates many V8 optimizations. Fastify’s edge comes from concrete engineering choices rather than micro-tweaks.
| Concern | Express | Fastify |
|---|---|---|
| Router | Linear array of layers | Radix tree (find-my-way) |
| Serialization | JSON.stringify | Compiled schema serializer |
| Validation | Manual / middleware | Built-in Ajv per route |
| Async errors | Needs wrappers | Native promise support |
| Logging | Add-on (e.g. morgan) | Built-in Pino, structured |
In typical JSON benchmarks Fastify handles two to three times the requests per second of Express for the same payload, mostly thanks to schema serialization and the faster router.
Best practices
- Always attach a
responseschema to hot routes — serialization speed is where Fastify’s biggest wins come from. - Structure features as encapsulated plugins, and reach for
fastify-pluginonly when something genuinely needs to be shared globally. - Put authentication and rate-limiting in
onRequesthooks so unauthorized requests are rejected before any parsing work. - Enable the built-in Pino logger and pass
request.idthrough to downstream calls for traceable, structured logs. - Use
app.decoratefor shared resources (database pools, clients) instead of importing singletons across files. - Validate environment configuration at boot and
await app.ready()in tests to ensure every plugin has loaded.