Picking a Node web framework feels like it should be a one-line answer. It isn't — but it also isn't a holy war. Fastify and Express make genuinely different bets about what a server's developer experience should look like, and which one fits depends a lot on the team you're picking it for.
This isn't a benchmark post. There are plenty of those. This is what changes day-to-day in the codebase, in code review, in onboarding, and in the kinds of bugs you ship.
The Setup In Both Frameworks
A "hello, validated" route in each:
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
const Body = z.object({ name: z.string().min(1) });
app.post('/users', (req, res) => {
const { name } = Body.parse(req.body);
res.status(201).json({ id: crypto.randomUUID(), name });
});
import Fastify from 'fastify';
const app = Fastify({ logger: true });
app.post('/users', {
schema: {
body: {
type: 'object',
properties: { name: { type: 'string', minLength: 1 } },
required: ['name'],
},
response: {
201: {
type: 'object',
properties: { id: { type: 'string' }, name: { type: 'string' } },
},
},
},
}, async (req, reply) => {
reply.code(201).send({ id: crypto.randomUUID(), name: req.body.name });
});
The Fastify version has more text, but every line of it shows up in three places at once: as runtime validation, as serialized output (Fastify uses fast-json-stringify to compile the response schema into a much faster serializer), and as a basis for OpenAPI generation via @fastify/swagger. In Express you'd hand-write each of those.
Schema-First Is The Real Difference
The performance gap between Fastify and Express is real but easy to overstate. The architectural gap is bigger and matters more day to day. Fastify treats the JSON Schema for each route as a first-class artifact. That schema validates inputs, serializes outputs, and documents the API. Express treats the schema as your problem, which means you usually wire zod into a middleware and accept that response serialization isn't checked at all.
The cost: you write JSON Schema, which is verbose. The win: you get one source of truth instead of three. Tools like fastify-type-provider-zod let you write zod and have Fastify use it as if it were JSON Schema, which is the path I usually recommend in 2024 — best of both ergonomics.
Plugins, Encapsulation, And register()
Express middleware is global by default. You add it once with app.use(), and it touches every request after that point. That's simple and famously easy to footgun (an auth middleware accidentally registered before a public route, etc.).
Fastify uses a plugin system with encapsulation. A plugin gets its own scope: middleware, decorators, hooks added inside it don't leak outside unless you opt in.
app.register(async (publicScope) => {
publicScope.get('/health', async () => ({ ok: true }));
});
app.register(async (privateScope) => {
privateScope.addHook('preHandler', authHook);
privateScope.get('/me', async (req) => req.user);
});
The /me route gets authHook. The /health route doesn't. There's no chain to read top-to-bottom and reason about — the boundary is the register() call. That's a different mental model, and once it clicks, large Fastify codebases stay manageable longer.
Express can do the same with Router() instances and per-route middleware, but nothing in the framework forces you to. In Fastify, the encapsulation is the path of least resistance.
Performance, Honestly
Fastify benchmarks run roughly 2–3× the requests-per-second of Express on synthetic JSON workloads. That's real and the schema-compiled serializer is most of the gap. In a real service, where most of your time is in the database, the framework difference often shrinks to single-digit percentages of total request latency.
When does the gap actually show up? When you have:
- Very chatty endpoints with small payloads (think internal microservices).
- A fixed CPU budget per request and you've already optimized everything else.
- A high enough request rate that 20% headroom matters for your bill.
For a typical CRUD API talking to Postgres? You won't tell the difference under load — both will be fine until your DB isn't.
Plugin Ecosystem
Express is older and has more middleware on npm. Fastify is younger and has a smaller, more curated set of official plugins (@fastify/cors, @fastify/helmet, @fastify/jwt, @fastify/multipart, @fastify/swagger, @fastify/rate-limit, etc.). The Fastify ecosystem isn't tiny — it's just narrower and more consistent.
The practical implication: if you're integrating an obscure SaaS that ships an Express middleware, you might need a tiny adapter to use it on Fastify. Fastify supports Express middleware via @fastify/express, but using it negates some performance and encapsulation wins.
Migration And Coexistence
Greenfield service? Pick whichever the team is more comfortable with. The teams I've seen succeed with Fastify treated the JSON Schema discipline as a feature; the teams that bounced back to Express usually didn't.
Migrating an existing Express app to Fastify is doable but rarely worth the cost unless you have a specific pain (e.g. throughput ceiling, or wanting OpenAPI without the manual maintenance). Migrate route-by-route inside a reverse proxy, not in one big rewrite.
A Quick Decision Heuristic
A short cheat sheet for "which one for this project?":
- Express if: the team already knows it, your performance budget is dominated by I/O, you want maximum middleware availability, or you want the most familiar Node experience for new hires.
- Fastify if: you want JSON Schema or zod-driven validation as a first-class concern, you want OpenAPI without hand-writing it, you want plugin encapsulation by default, or you're building many small services where the per-request CPU win compounds.
- Either is fine if: you have a typical CRUD service, a small team, and reasonable test coverage — neither will be the thing that fails you.
Don't pick a framework for a benchmark you'll never hit. Pick it for the constraints it puts on the codebase.
A One-Sentence Mental Model
Express hands you a router and a middleware list and trusts you to keep them tidy; Fastify hands you a schema-first plugin tree and trusts the structure to keep itself tidy — pick the discipline your team will actually maintain a year from now.






