Context
Fastify and Express are both web frameworks for Node, but they start from different priorities. Express is the incumbent: a tiny core built around middleware, where practically everything (body parsing, authentication, CORS, validation) is a function you slot into the (req, res, next) chain. That radical simplicity and over a decade on the road made Express the de facto standard of Node, with a middleware ecosystem that likely already has a ready answer for any need. The trade-off is that Express itself validates nothing, serializes nothing in an optimized way, and, until v5, didn't catch errors from async handlers for you.
Fastify starts from a different thesis: performance and an explicit data contract as part of the framework, not something bolted on top. It was designed for high throughput and low per-request overhead, ships with built-in JSON Schema validation, and uses that same schema to serialize the response far faster than the default JSON.stringify. It's been async-first from the start and has first-class TypeScript support. Extensibility comes from a plugin system with encapsulation (each plugin has its own scope of decorators, hooks, and routes), instead of Express's linear middleware chain. Worth noting that both are now on v5: Express 5 shipped after years stalled on 4.x and brought, among other things, automatic forwarding of rejected promises from async handlers to the error handler.
The deciding variable is what you value: ubiquity and a huge ecosystem, or performance and a built-in typed contract.
When to choose Fastify
Fastify makes the most sense when performance and an explicit data contract matter from the start. The JSON schema isn't decoration: it validates the request body, query, and params before the handler runs, and it's reused to serialize the response, a path that in practice beats generic JSON.stringify on high-volume routes. Add the async-first model and first-class TypeScript support (with schema-to-type inference via a type provider like typebox or json-schema-to-ts), and you get a framework that treats validation, serialization, and types as part of the core.
import Fastify from "fastify";
const app = Fastify();
app.post("/items", {
schema: {
body: {
type: "object",
required: ["name", "price"],
properties: {
name: { type: "string" },
price: { type: "number" },
},
},
},
}, async (request) => {
const { name, price } = request.body;
return { name, total: Math.round(price * 1.1 * 100) / 100 };
});
await app.listen({ port: 3000 });
In the example, the schema declares that name and price are required and typed: a request without name or with a non-numeric price gets a structured 400 before the handler is ever called, without a line of manual validation. The handler is async, and Fastify handles the response lifecycle and error capture for you. The plugin system keeps this organized as the application grows: each plugin encapsulates its routes, hooks, and decorators in its own scope, which avoids the global coupling typical of a long middleware chain. The entry cost is learning the model of schemas, hooks, and plugins, but in services where throughput and a typed API contract are requirements, that investment pays off. At around 9.5 million downloads per week, Fastify is much smaller than Express in adoption, but established as the performance alternative.
When to choose Express
Express remains the right choice when ubiquity and ecosystem weigh more than raw throughput. The core is minimal and the mental model is one of the simplest in Node: a request comes in, passes through a queue of middleware, and a response goes out. That, combined with over ten years as the de facto standard, means practically every problem (sessions, uploads, rate limiting, authentication with any provider) already has a mature middleware, a tutorial, and a Stack Overflow answer. At around 114 million downloads per week, it's the base the most people know and the easiest to hire for.
import express from "express";
const app = express();
app.use(express.json());
app.post("/items", (req, res) => {
const { name, price } = req.body;
if (!name || typeof price !== "number") {
return res.status(400).json({ error: "name and price are required" });
}
res.json({ name, total: Math.round(price * 1.1 * 100) / 100 });
});
app.listen(3000);
The same endpoint makes the difference clear: in Express validation is your responsibility (done by hand here, in practice via a library like zod or express-validator), and there's no optimized serialization or built-in schema. In exchange, you get full transparency over the request chain and access to the largest middleware catalog in the ecosystem. v5, released recently after a long stretch on 4.x, modernizes important points: rejected promises from async handlers are now forwarded automatically to the error handler (on 4.x you needed a wrapper or try/catch in every route), plus adjustments to routing and route syntax. For many services, moderate-traffic APIs, and teams that already know the model, Express remains the most predictable and best-documented base.
Verdict
Reduced to its essentials, the choice comes down to one question: do you need performance and a built-in typed data contract, or ubiquity and Node's largest ecosystem? If throughput under load, schema validation, and fast serialization are requirements, and you want them in the framework's core instead of assembled from middleware, Fastify was designed for exactly that case and delivers types, validation, and schema docs as part of the package. It's the natural bet for new APIs where performance and contract matter from the first endpoint.
If what weighs more is finding ready-made middleware for everything, hiring people who already know the model, and having the ecosystem's richest documentation, Express rarely disappoints, and v5 closed the most annoying gap by handling async errors by default. Don't rewrite a healthy Express app to Fastify just for the promise of a benchmark: the throughput gain only pays off when throughput is actually the bottleneck. Choose Fastify for performance and the built-in schema, Express for ubiquity and the ecosystem, and let the service's profile decide.