This article is published in English.
Express vs Fastify in 2026: A Practical Node.js Framework Comparison
This guide compares Express and Fastify on performance, validation, ecosystem, and error handling, plus covers key Express 5 breaking changes.
Picture a team kicking off a fresh Node.js backend.
They start researching frameworks, and two names dominate the conversation almost immediately:
Express.js and Fastify.
Then the benchmark charts show up.
Across many synthetic tests, Fastify posts noticeably higher throughput numbers.
The natural reaction becomes:
"If Fastify wins on speed, why would anyone keep using Express?"
It's a fair question on the surface.
But picking a backend framework rarely comes down to a single metric like that.
Raw requests-per-second is only one piece of the puzzle. You also need to weigh the surrounding ecosystem, how middleware works, built-in validation, TypeScript support, how costly a migration would be, the day-to-day developer experience, any existing codebase you're maintaining, and what your specific application actually demands.
There's also a second layer to this comparison worth noting.
Express 5 is now officially out.
After a long run with Express 4, this new major version introduces a handful of changes that anyone maintaining an older Express codebase needs to be aware of.
With that context set, let's dig into what genuinely separates Express from Fastify — and, more usefully, the situations where each one makes sense.
First: What exactly are Express and Fastify?
Both frameworks exist to help you build web servers on top of Node.js.
At their core, they save you from writing directly against Node's lower-level HTTP module by giving you a cleaner API for defining routes and handling requests.
Here's what a minimal Express server looks like:
const express = require("express");
const app = express();
app.get("/users", (req, res) => {
res.json([
{ id: 1, name: "Neha" },
{ id: 2, name: "Rahul" }
]);
});
app.listen(3000);
Fastify's starting point looks quite similar:
const fastify = require("fastify")({
logger: true
});
fastify.get("/users", async (request, reply) => {
return [
{ id: 1, name: "Neha" },
{ id: 2, name: "Rahul" }
];
});
fastify.listen({ port: 3000 });
Notice anything?
Neither example is particularly intimidating.
The real divergence between the two only becomes obvious once your application grows past this toy-example stage.
Express vs Fastify: The Big Picture
Let's look at why the differences between these two frameworks actually matter in practice.
1. Performance: Fastify Has the Advantage
This is likely the single biggest reason Fastify keeps coming up in these conversations.
Performance and minimal overhead were core design goals from the start for Fastify.
Its internals rely heavily on schemas for both validating incoming data and serializing outgoing responses, which can meaningfully boost throughput for API-heavy workloads. Fastify's own documentation recommends using JSON Schema specifically for route validation and response serialization.
That said, there's an important caveat here.
Don't take a single benchmark result and jump straight to the conclusion that:
Fastify automatically means a 3x faster application.
Most framework benchmarks are measuring the overhead of the framework itself, under tightly controlled conditions.
Fastify's own maintainers acknowledge that their published benchmark is a synthetic "hello world" style test, and they explicitly suggest benchmarking your own real application if performance is a priority for you.
Consider a request flow that looks like this:
Request
↓
Authentication
↓
Database query
↓
Redis
↓
External API
↓
Business logic
↓
Response
If your database call alone takes 80 milliseconds, shaving off a sliver of framework overhead isn't going to transform that endpoint's speed in any dramatic way.
So the real question to ask yourself is:
Is my application actually bottlenecked by CPU or framework overhead?
In other words: is the framework itself the thing slowing requests down? If the answer is yes, switching to Fastify has a much stronger justification.
If the answer is no, then generic framework benchmarks probably shouldn't be your deciding factor.
2. Validation: This Is Where Fastify Gets Interesting
Imagine your API is designed to accept a payload shaped like this:
{
"name": "Neha",
"age": 25
}
Naturally, you'd want to reject a malformed version of that same payload, something like:
{
"name": 123,
"age": "hello"
}
In the Express world, teams typically reach for an additional library to handle this kind of request validation. Fastify takes a different approach: schema-based validation is baked directly into the framework itself.
Here's what that looks like in practice:
const schema = {
body: {
type: "object",
required: ["name", "age"],
properties: {
name: { type: "string" },
age: { type: "integer" }
}
}
};
fastify.post("/users", { schema }, async (request, reply) => {
return { message: "User created" };
});
Fastify lets you define JSON Schema definitions for several different parts of a request and response cycle, including:
- the request body
- query parameters
- route parameters
- headers
- response serialization
Under the hood, Fastify relies on Ajv to power this validation layer, and the same schema definitions can also be used to speed up how responses get serialized.
Ajv, short for "Another JSON Schema Validator," is a fast, standards-compliant library for validating JavaScript data objects against JSON Schema definitions, and it works both in Node.js and in the browser.
This schema-first approach becomes particularly valuable once your API surface grows to include a lot of structured input and output.
3. Express Has Something Fastify Can't Easily Replace: Its Ecosystem
Express has existed since 2010, which means there's a massive body of accumulated knowledge, tooling, and community experience built around it.
- Need authentication? There's a package for it.
- Need logging? There's a package for it.
- Need CORS handling? There's a package for it.
- Need validation? There's a package for it.
- Stuck on some obscure bug? Odds are someone else has already hit it and documented a fix.
This kind of ecosystem depth matters more than it might seem at first glance.
Picture yourself joining a company whose backend has been running in production for six years. You don't get to pick the framework from scratch, you inherit whatever is already there, which might look something like this:
Express
├── 200+ routes
├── authentication middleware
├── custom middleware
├── logging
├── validation
├── monitoring
└── lots of business logic
Would rewriting all of that just because Fastify benchmarks faster actually make sense? Probably not.
Migrating an existing codebase always carries a cost, and any real engineering decision needs to weigh that cost against the expected benefit.
4. Middleware vs Plugins
There's also a deeper architectural distinction between the two frameworks.
Express is built around the concept of middleware. A typical setup might look like:
app.use(authMiddleware);
app.use(loggingMiddleware);
app.use(express.json());
Each incoming request then passes sequentially through these middleware functions.
Fastify, by contrast, uses a plugin-based architecture built on hooks and encapsulation. Conceptually, the flow looks more like this:
Request
↓
Fastify
↓
Hooks
↓
Plugins
↓
Route
↓
Response
For applications that are designed from the start around Fastify's model, this structure can make larger codebases easier to organize and reason about.
That said, there's a trade-off. If you've spent years building mental models around Express-style middleware, adapting to Fastify's plugin and hook system may feel unfamiliar at first.
5. Error Handling
In Express 4, handling errors from asynchronous route handlers usually required manually forwarding them. A typical pattern looked like this:
app.get("/user/:id", async (req, res, next) => {
try {
const user = await getUserById(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
});
Express 5 simplifies this considerably. Now, promise rejections thrown from route handlers or middleware are automatically passed along to your error-handling middleware, so you can write something much leaner:
app.get("/user/:id", async (req, res) => {
const user = await getUserById(req.params.id);
res.json(user);
});
If getUserById() throws or rejects, Express 5 catches that automatically and routes it to your error handler, no explicit try/catch or next(err) call required.
On its own, this looks like a minor syntactic convenience. But across a codebase with hundreds of routes, this kind of small cleanup adds up to noticeably tidier, less error-prone code.
Express 5: What Actually Changed?
Express 5 landed in October 2024, so heading into 2026 it's no longer a fresh release. Still, a large number of production apps run on Express 4, which means understanding what shifted between versions matters a great deal for anyone planning an upgrade.
And yes, there are breaking changes you need to be aware of.
1. Optional Route Parameters Changed
In Express 4 you might have written a route like this:
app.get("/:file.:ext?", handler);
Express 5 replaces that pattern with:
app.get("/:file{.:ext}", handler);
The old ? notation for marking a parameter optional no longer works.
Why the switch?
The new syntax makes it clearer, at a glance, which part of the path is optional.
It's a subtle change on paper, but it can quietly break dozens of routes in a large, established application.
2. Wildcard Routes Changed
Previously, a catch-all route might look like this:
app.get("/*", handler);
Express 5 requires wildcards to be named:
app.get("/*splat", handler);
If you need that wildcard to also match the root path /, wrap it like this:
app.get("/{*splat}", handler);
This is yet another small syntax shift that can silently break existing routing logic during an upgrade.
3. Regular Expression Route Patterns Changed
Express 5 drops support for several of the old string-based patterns that leaned on regex-style characters.
For instance, rather than embedding alternative segments directly into a single path string, you now typically pass an array of paths:
app.get(
["/discussion/:slug", "/page/:slug"],
handler
);
The intent behind this change is to keep route matching explicit and predictable rather than relying on regex-like shortcuts.
4. req.body Can Now Be undefined
This is a subtle one that's easy to overlook.
In Express 4, it was common to assume that:
req.body
would default to an empty object even before any parsing happened.
Under Express 5, if nothing parsed the body, req.body can simply be undefined.
That means a defensive check like this becomes relevant depending on the route:
if (!req.body) {
return res.status(400).send("Request body required");
}
It's a minor behavioral tweak, but exactly the kind of small detail that can introduce hard-to-trace bugs after an upgrade.
5. express.urlencoded() Changed
The default for the extended option is now false.
So rather than depending on the implicit default:
app.use(express.urlencoded());
you'll want to set it explicitly if your app relies on the older behavior:
app.use(
express.urlencoded({
extended: true
})
);
Another detail worth auditing carefully when migrating an existing codebase.
6. Body Parser Changes
Express 5 also tidied up how body parsing works internally.
You can now write:
app.use(express.json());
app.use(
express.urlencoded({
extended: false
})
);
instead of stitching together separate body-parser middleware packages as before.
On top of that, Express 5 adds support for Brotli-compressed request bodies and lets you configure the maximum depth allowed for URL-encoded payloads.
7. Some Old Response APIs Were Removed
Imagine an older Express app containing something like:
res.send({
message: "Success"
}, 200);
Under Express 5, the expected form is:
res
.status(200)
.send({
message: "Success"
});
Likewise, the old shortcut:
res.redirect("back");
has been removed entirely.
The official migration guide suggests reading the referrer header manually and falling back to a default path when it's missing.
None of these individual changes is particularly hard to fix.
But picture a legacy codebase with thousands of calls written the old way.
At that scale, upgrading stops being a quick dependency bump and turns into a real engineering effort in its own right.
So, Express or Fastify: Which One Wins?
At this point you have enough context to make a real decision.
I wouldn't base it purely on:
"Which one benchmarks faster?"
Instead, start by looking at the application itself.
Reasons to Stick With Express:
- You're just getting started with backend development in Node.js.
- Your team is already comfortable with Express.
- You're maintaining or extending an existing Express codebase.
- Your project relies heavily on Express-style middleware.
- You want access to the widest possible ecosystem of packages.
- Raw performance isn't the main constraint you're facing.
- You prefer something simple and flexible over something opinionated.
Reasons to Look at Fastify:
- You're starting a new API-focused service from scratch.
- Throughput and minimizing framework overhead actually matter.
- You want validation driven directly by schemas.
- You want serialization handled through schemas as well.
- You're building out microservices.
- You like working with a plugin-based architecture.
- You're fine adopting a somewhat younger ecosystem.
There's nothing inherently wrong with picking Express even for a large-scale system.
Being a "large application" doesn't automatically mean Fastify is the right call.
What surrounds the framework — your overall architecture — usually matters far more than the framework choice itself.
A Basic Way to Reason Through It
Here's a rough mental flow for making the call:
Start
│
▼
Is this an existing app?
/ \
Yes No
│ │
▼ ▼
Already using Need very high
Express? throughput?
/ \ / \
Yes No Yes No
│ │ │ │
▼ ▼ ▼ ▼
Keep Evaluate Fastify Evaluate
Express migration both
Before locking in a final answer, there's one more question worth asking:
What problem am I actually trying to fix?
If your existing Express app feels slow, resist the urge to blame the framework right away.
Profile it first.
The real culprit might be:
Slow API
↓
Database query
↓
Missing index
or:
Slow API
↓
External API
↓
3-second response time
or:
Slow API
↓
Expensive business logic
↓
CPU bottleneck
Swapping frameworks won't resolve any of these underlying issues on its own.
Where I Land on This
If you were kicking off a small new Node.js project today, dismissing Express just because Fastify posts better benchmark numbers wouldn't make much sense. Express carries a massive ecosystem, a straightforward mental model, and years of collective knowledge built around it.
That said, Fastify shouldn't be overlooked either. For a new service where throughput, schema validation, serialization, and minimal framework overhead genuinely matter, Fastify deserves serious consideration.
And if you inherited an existing Express 4 application? Rewriting it purely because Fastify is faster would be the wrong move.
The better approach is to understand the application first, measure where the actual bottlenecks live, verify Express 5 compatibility, run through the migration tests, and only then decide whether switching frameworks delivers enough real value to justify the effort.
That's probably the core takeaway here.
The framework with the best benchmarks isn't automatically the right framework for your situation.
The right framework is whichever one solves your actual problem while adding the least unnecessary complexity.