Fastify 5.0: The Node.js Framework Faster Than Express
Fastify 5.0 has arrived with a promise that sounds too good to be true: a Node.js framework that can outpace Express in raw speed while keeping the developer experience pleasant. If you’ve spent any time wrestling with request latency, memory footprints, or the overhead of middleware, you’ll appreciate the design choices Fastify makes. In this article we’ll explore the core concepts that make Fastify fast, walk through a couple of real‑world examples, and sprinkle in some pro tips you can apply today.
Why Fastify Is Faster Than Express
At its heart Fastify follows a “schema‑first” philosophy. By requiring you to declare request and response schemas up front, the framework can compile highly optimized validation and serialization functions ahead of time. This eliminates the reflective, per‑request checks that Express performs with generic middleware.
Fastify also embraces a low‑overhead plugin system. Each plugin runs in its own encapsulated context, allowing the core to avoid global state and reducing the amount of code that needs to be parsed on each request. The result is a smaller call stack and less garbage collection pressure.
Finally, Fastify leverages the latest Node.js features, such as the http2 module and async hooks, to keep I/O bound work non‑blocking while still providing a synchronous‑looking API. The combination of compile‑time optimizations, modular plugins, and modern Node internals gives Fastify its performance edge.
Getting Started: A Minimal Fastify Server
Let’s spin up the most basic Fastify server. The code fits on a single screen, yet it already demonstrates the framework’s core API: fastify() to create an instance, .get() to register a route, and .listen() to start listening.
const fastify = require('fastify')({ logger: true });
fastify.get('/ping', async (request, reply) => {
return { pong: true };
});
fastify.listen({ port: 3000 }, (err, address) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.log.info(`Server listening at ${address}`);
});
Notice the use of an async handler. Fastify automatically resolves the returned object and serializes it as JSON, eliminating the need for res.json() or res.send() that you’d write in Express.
Running the Example
Save the snippet as server.js, run npm init -y && npm i fastify@5, and start it with node server.js. Hit http://localhost:3000/ping in your browser or curl to see the JSON response. In a typical Express setup you’d need an extra app.use(express.json()) middleware; Fastify handles that out of the box.
Pro tip: Enable logger: true only in development. In production you can pipe logs to a structured logging service for better observability without the overhead of console I/O.
Schema‑Based Validation and Serialization
Fastify’s speed boost shines when you add JSON schemas. Define a schema once, and Fastify compiles both a validator for incoming payloads and a serializer for outgoing data. This avoids the runtime reflection cost that Express middleware suffers from.
const userSchema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 }
}
};
fastify.post('/users', {
schema: {
body: userSchema,
response: {
201: userSchema
}
}
}, async (request, reply) => {
const user = request.body;
// Simulate DB insert
return reply.code(201).send(user);
});
The schema.body block validates the request payload before your handler runs. If validation fails, Fastify returns a 400 error with a detailed message, saving you from writing repetitive checks.
On the response side, Fastify uses the same schema to serialize the object. This means only the fields you declared are sent back, preventing accidental data leaks—a security win that also reduces payload size.
Pro tip: Keep schemas in separate files and import them. This makes them reusable across plugins and helps maintain a clean codebase, especially when you have dozens of endpoints.
Plugin Architecture: Building Scalable Applications
Fastify’s plugin system is one of its most powerful features. A plugin can register routes, decorators, or even its own sub‑plugins, all while staying isolated from the global scope. This isolation prevents accidental side effects and makes testing straightforward.
Below is a simple “auth” plugin that adds a verifyToken decorator and a pre‑handler hook. The plugin can be reused in any Fastify instance without polluting the root.
// auth-plugin.js
module.exports = async function (fastify, opts) {
fastify.decorate('verifyToken', async (request) => {
const auth = request.headers['authorization'];
if (!auth) throw fastify.httpErrors.unauthorized('Missing token');
// Dummy verification – replace with JWT logic
if (auth !== 'Bearer secret-token') {
throw fastify.httpErrors.forbidden('Invalid token');
}
});
fastify.addHook('preHandler', async (request, reply) => {
await fastify.verifyToken(request);
});
};
To use the plugin, register it on the Fastify instance and then define routes that automatically benefit from the authentication check.
const fastify = require('fastify')({ logger: true });
const authPlugin = require('./auth-plugin');
fastify.register(authPlugin);
fastify.get('/protected', async (request, reply) => {
return { secret: 'You have access!' };
});
fastify.listen({ port: 4000 }, (err, address) => {
if (err) process.exit(1);
fastify.log.info(`Running on ${address}`);
});
Notice how the /protected route never mentions authentication logic. The plugin’s preHandler hook runs before every request, keeping your route handlers clean and focused on business logic.
Pro tip: When building large services, group related routes and plugins into “domains” (e.g., user, payment, analytics). Register each domain as a separate plugin to keep the main server file tidy.
Real‑World Use Case: Building a High‑Throughput API Gateway
Imagine you need an API gateway that proxies requests to multiple microservices, adds request tracing, and enforces rate limiting. Fastify’s low overhead makes it a natural fit for this pattern, where every millisecond counts.
First, we’ll add a rate‑limiting plugin. Fastify ships with fastify-rate-limit, which integrates seamlessly with the core.
const rateLimit = require('@fastify/rate-limit');
fastify.register(rateLimit, {
max: 100, // max 100 requests
timeWindow: '1 minute' // per minute per IP
});
Next, we’ll create a proxy route that forwards incoming traffic to an internal service using the built‑in http module. Fastify’s reply.from() method (provided by @fastify/http-proxy) handles streaming efficiently, preserving the original request’s back‑pressure.
const proxy = require('@fastify/http-proxy');
fastify.register(proxy, {
upstream: 'http://localhost:5000',
prefix: '/service', // all /service/* requests are proxied
http2: false // keep it simple for this demo
});
Finally, add a simple tracing decorator that logs a unique request ID. This ID propagates through all downstream services, making debugging a breeze.
const { v4: uuidv4 } = require('uuid');
fastify.decorateRequest('id', null);
fastify.addHook('onRequest', async (request) => {
request.id = uuidv4();
request.headers['x-request-id'] = request.id;
});
fastify.addHook('onResponse', async (request, reply) => {
fastify.log.info({ reqId: request.id, status: reply.statusCode }, 'Request completed');
});
Putting it all together, the gateway can handle thousands of concurrent requests, enforce rate limits, and provide end‑to‑end traceability—all with a fraction of the CPU cycles required by an equivalent Express setup.
Pro tip: Enable HTTP/2 in Fastify when your clients support it. The http2 option reduces latency for TLS handshakes and allows multiplexed streams over a single connection.
Performance Benchmarks: Fastify vs. Express
Benchmarks are always a moving target, but recent community tests give a clear picture. In a simple “GET /ping” scenario, Fastify 5.0 consistently serves around 1.2‑1.5 million requests per second on a modest 4‑core VM, while Express hovers near 800 k rps under the same conditions.
When you add schema validation and JSON serialization, the gap widens. Fastify’s compiled validators keep the CPU usage under 30 % of what Express’s runtime checks consume. This translates into lower cloud costs, especially for traffic‑heavy APIs.
It’s worth noting that raw speed isn’t the only metric. Fastify’s plugin encapsulation reduces memory leaks, and its built‑in logging integrates with tools like Pino for minimal overhead. Together, these factors make Fastify a more sustainable choice for production workloads.
Advanced Topics: Custom Serializers and Hooks
Fastify lets you plug in custom serializers for specific content types. For example, if you need to serve protobuf or MessagePack, you can register a serializer that Fastify will invoke automatically based on the Accept header.
fastify.addContentTypeParser('application/msgpack', { parseAs: 'buffer' }, (req, payload, done) => {
// Use msgpack library to decode
const decoded = msgpack.decode(payload);
done(null, decoded);
});
fastify.addSerializer('application/msgpack', (payload) => {
return msgpack.encode(payload);
});
fastify.get('/binary', async () => {
return { data: 'binary payload' };
});
The above snippet adds a parser and serializer for MessagePack, enabling content‑negotiation without extra route logic. This pattern is especially useful for IoT or high‑frequency trading APIs where binary formats shave off precious bytes.
Hooks are another place where Fastify’s design shines. Beyond the standard onRequest, preHandler, and onResponse, you can create custom lifecycle hooks to inject behavior at any stage. For instance, a “metrics” hook can record request duration and push it to Prometheus.
fastify.addHook('onResponse', async (request, reply) => {
const duration = reply.getResponseTime();
// Assume promClient is set up elsewhere
requestDurationHistogram.observe({ route: request.routerPath }, duration);
});
By centralizing such cross‑cutting concerns, you keep route handlers pure and your codebase easier to maintain.
Pro tip: Use Fastify’srequest.routerPathinstead of the raw URL when labeling metrics. It groups similar routes together (e.g.,/users/:id) and prevents metric explosion.
Testing Fastify Applications
Fastify ships with a built‑in testing utility that spins up an instance without binding to a network port. This makes unit tests fast and deterministic. Pair it with tap or jest for a smooth developer experience.
const build = require('./app'); // your Fastify instance creator
const { test } = require('tap');
test('GET /ping returns pong', async (t) => {
const fastify = await build(); // builds without listening
const response = await fastify.inject({
method: 'GET',
url: '/ping'
});
t.equal(response.statusCode, 200);
t.same(JSON.parse(response.payload), { pong: true });
await fastify.close();
});
The inject() method bypasses the network stack, delivering the request directly to the router. This approach is orders of magnitude faster than making real HTTP calls and ensures your tests are not flaky due to port conflicts.
Migration Path: From Express to Fastify
If you already have an Express codebase, the migration can be incremental. Start by wrapping existing routes in a Fastify plugin, using the fastify-express compatibility layer if you need a temporary bridge.
const fastify = require('fastify')();
const fastifyExpress = require('@fastify/express');
fastify.register(fastifyExpress).after(() => {
const expressApp = fastify.express;
expressApp.get('/legacy', (req, res) => {
res.send('Still works with Express');
});
});
fastify.listen(3000);
Once the bridge is in place, you can progressively replace Express routes with native Fastify handlers, benefiting from schema validation and better performance step by step.
Pro tip: Keep a “feature flag” that toggles between the old Express route and the new Fastify route during migration. This lets you roll back instantly if something unexpected occurs.
Best Practices for Production Deployments
- Enable HTTP/2 when TLS is mandatory; it reduces handshake latency.
- Use Pino as the logger (Fastify’s default) and pipe logs to a centralized system like Loki or Elastic.
- Set
trustProxyappropriately if you sit behind a load balancer, so Fastify can correctly resolve client IPs for rate limiting. - Monitor process metrics (CPU, heap, event loop delay) with
@fastify/under-pressureto trigger graceful shutdowns before crashes.
Here’s a quick snippet that adds the under‑pressure plugin with sensible defaults:
fastify.register(require('@fastify/under-pressure'), {
maxEventLoopDelay: 1000, // 1 second
maxHeapUsedBytes: 100 * 1024 * 1024, // 100 MB
maxRssBytes: 200 * 1024 * 1024, // 200 MB
exposeStatusRoute: true
});
When the process exceeds any of these thresholds, Fastify automatically returns a 503 response and logs the incident, giving you a safety net during traffic spikes.
Conclusion
Fastify 5.0 delivers on its promise of being faster than Express without sacrificing developer ergonomics. By embracing schema‑first validation, a lightweight plugin system, and modern Node.js features, it reduces latency, memory usage, and boilerplate. Whether you’re building a simple microservice, a high‑throughput API gateway, or migrating a legacy Express app, Fastify provides the tools to write clean, performant, and maintainable code.
Start experimenting with the