Express 5 vs Fastify 5: which Node.js framework to pick in 2026
Express 5 and Fastify 5 compared on support status, validation, error handling, upgrades and performance, with the same API built and tested in both.
7 min read

In this article
Pick Express 5 if your team already knows Express, you depend on its huge middleware ecosystem, or you are upgrading an existing Express 4 app. Pick Fastify 5 for a new JSON API where you want request validation, response serialisation, structured logging and in-process testing built in rather than assembled from packages. Both are actively maintained, both run well on Node.js 24, and for most database-backed APIs the framework is not your bottleneck.
This comparison uses the official migration guides and support pages, npm release data checked on 9 October 2026, and the same small API built in both frameworks and run on Node.js 24.19.0 with Express 5.3.0 and Fastify 5.12.5.
Where each project stands in October 2026
| Express 5 | Fastify 5 | |
|---|---|---|
| First 5.x release | 5.0.0, 9 September 2024 | 5.0.0, 17 September 2024 |
| Latest on npm (9 Oct 2026) | 5.3.0 | 5.12.5 |
| Minimum Node.js | 18 | 20 |
| Previous major | 4.x in maintenance since 1 April 2025; still receiving releases (4.22.3 on 14 September 2026) | 4.x support ended 30 June 2025 |
| Next major | None announced | 6.0.0 in alpha on the next tag |
| Validation | Bring your own (zod, Joi, express-validator and others) | Built in: JSON Schema with Ajv v8 |
| Serialisation | JSON.stringify | fast-json-stringify when a response schema is set |
| Logging | Bring your own | Built in (Pino) |
| Async errors | Rejected promises go to error middleware | Async handlers are native; thrown errors go to the error handler |
Express's published support policy says a major version stays ACTIVE for at least 12 months after being tagged latest, and the previous major then gets 12 months of MAINTENANCE with security and high-priority fixes. Version 5.1.0 became latest on 31 March 2025, which started that clock for 4.x; the Express team gave 4.x an end-of-life date of no sooner than 1 October 2026 and said it may extend maintenance. If you still run Express 4, plan the upgrade now.

The same API in both frameworks
Each version exposes GET /users/:id and POST /users, returns 404 for unknown users, validates the request body and handles a database error.
Express 5
// express-app.mjs
import express from 'express';
const app = express();
app.use(express.json({ limit: '100kb' }));
app.get('/users/:id', async (req, res) => {
const user = await findUser(req.params.id); // a rejection goes to the error handler
if (!user) return res.status(404).json({ error: 'not found' });
res.json(user);
});
app.post('/users', (req, res) => {
const { name } = req.body ?? {};
if (typeof name !== 'string' || name.length === 0) {
return res.status(400).json({ error: 'name is required' });
}
res.status(201).json({ id: '2', name });
});
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).json({ error: 'internal error' });
});
async function findUser(id) {
if (id === 'boom') throw new Error('database unavailable');
return id === '1' ? { id: '1', name: 'Asha' } : null;
}
app.listen(3002, () => console.log('express on 3002'));Fastify 5
// fastify-app.mjs
import Fastify from 'fastify';
const app = Fastify({ logger: false });
app.get('/users/:id', {
schema: {
params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
},
}, async (request, reply) => {
const user = await findUser(request.params.id);
if (!user) return reply.code(404).send({ error: 'not found' });
return user;
});
app.post('/users', {
schema: {
body: {
type: 'object',
properties: { name: { type: 'string', minLength: 1 } },
required: ['name'],
additionalProperties: false,
},
response: {
201: {
type: 'object',
properties: { id: { type: 'string' }, name: { type: 'string' } },
},
},
},
}, async (request, reply) => {
reply.code(201);
return { id: '2', name: request.body.name, internalNote: 'not serialised' };
});
async function findUser(id) {
if (id === 'boom') throw new Error('database unavailable');
return id === '1' ? { id: '1', name: 'Asha' } : null;
}
await app.listen({ port: 3003 });
console.log('fastify on 3003');What came back
# Express 5 (port 3002)
GET /users/1 {"id":"1","name":"Asha"}
GET /users/9 {"error":"not found"}
GET /users/boom {"error":"internal error"}
POST /users {} {"error":"name is required"}
POST /users {"id":"2","name":"Ravi"} 201
# Fastify 5 (port 3003)
GET /users/1 {"id":"1","name":"Asha"}
GET /users/9 {"error":"not found"}
GET /users/boom {"statusCode":500,"error":"Internal Server Error","message":"database unavailable"}
POST /users {} {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body must have required property 'name'"}
POST /users {"id":"2","name":"Ravi"} 201Three differences stand out:
- Validation. Express needs hand-written checks or a library. Fastify rejected the empty body from the schema alone, with a structured 400 response.
- Response filtering. The Fastify handler returned an extra
internalNotefield, and it never reached the client, because only properties in the response schema were serialised. The Fastify documentation recommends output schemas to prevent accidental disclosure of sensitive data and says they can drastically increase throughput. - Error output. Fastify's default error response included the original error message,
database unavailable. That can leak internal details. Add your own error handler before going to production:
app.setErrorHandler((error, request, reply) => {
if (error.validation) return reply.code(400).send({ error: error.message });
request.log.error(error);
reply.code(500).send({ error: 'internal error' });
});With that handler, the same failing route returned 500 {"error":"internal error"} in our test, which we ran with Fastify's built-in app.inject(), so no network port was needed.
What changed in Express 5, if you are upgrading
Express 5 is mostly a clean-up release, but several changes break Express 4 code. From the official migration guide:
- Async errors are handled. A rejected promise from a handler or middleware is passed to the error middleware, so you no longer need wrapper packages.
- Route syntax changed (path-to-regexp 8). Wildcards must be named:
/*splatinstead of/*, and optional parts use braces:/:file{.:ext}instead of?. Regular expression characters in string paths are no longer supported. req.bodyisundefinedwhen no body parser ran, rather than{}.- The default query parser is now "simple", so
?a[b]=1no longer becomes a nested object, andreq.queryis read-only. express.urlencoded()defaults toextended: false, andexpress.staticignores dotfiles by default.- Removed APIs:
app.del(),req.param(),res.sendfile(), and signatures likeres.json(obj, status); useres.status(status).json(obj).
We checked the route changes directly. An Express 4 style wildcard now throws at registration time, which at least makes the problem loud:
Express 4 wildcard rejected: Missing parameter name at index 6: /old/*; visit https://git.new/pathToRegexpError for info
/files/a/b/c.txt {"splat":["a","b","c.txt"]}
/docs/readme {"file":"readme"}
/docs/readme.md {"file":"readme","ext":"md"}
{"a[b]":"1"} # simple query parser: no nested object
{"body":"undefined"} # no body parser registeredNote that a named splat arrives as an array of path segments, not a single string.

What changed in Fastify 5
Fastify 5 requires Node.js 20 or later and is stricter about schemas: the migration guide says you must provide a full JSON Schema for querystring, params, body and response, including the type property, and the jsonShortHand option is gone. A custom logger instance now goes in loggerInstance rather than logger. Treat schemas as application code: Fastify compiles them with new Function(), and its documentation warns that this is unsafe with user-provided schemas.
Performance: what actually matters
Fastify was designed for low per-request overhead, and its schema-based serialisation is a real advantage for large JSON responses. But in a typical API each request also waits on a database or another service, and that wait usually dwarfs the framework's share. Benchmark your own routes, with your database, before choosing on speed alone. If you do find that framework overhead matters, Fastify's response schemas are the first lever to pull.
How to choose
- Existing Express 4 app: upgrade to Express 5. It is far less work than a rewrite, and the official migration guide lists every breaking change.
- New public JSON API: Fastify 5. Validation, serialisation, logging and
inject()testing are there from the first line, and the schemas double as documentation. - Server-rendered site, or heavy use of Express middleware such as Passport strategies: Express 5.
- Team of mixed experience: Express's smaller surface is easier to learn; Fastify's plugin encapsulation pays off as the codebase grows.
Gotchas
- Fastify's default 500 response exposes error messages. Set an error handler.
- Express 5 still has no request validation. Upgrading does not make an unvalidated API safe; add a schema library.
- Middleware compatibility. Fastify does not run Express middleware natively; check that a Fastify plugin exists for each piece you rely on before porting.
- Body size limits. Set them explicitly in both frameworks (
express.json({ limit }), Fastify'sbodyLimit) to match what your API accepts.
What to do next
If you are on Express 4, create a branch, upgrade to the latest Express 5 release, and search for app.get('*', /*, req.param( and res.json( calls with two arguments; your tests will find the rest. If you are starting fresh, build two or three real endpoints in Fastify with full request and response schemas, test them with app.inject(), and decide after a day of real work rather than from a benchmark chart.
Frequently asked questions
Is Express 5 stable and ready for production?
Yes. Express 5.0.0 was released on 9 September 2024, 5.1.0 became the default
latestnpm release on 31 March 2025, and 5.3.0 was the latest release on 9 October 2026.Is Express 4 still supported?
Express 4 has been in maintenance since 1 April 2025, receiving security and high-priority fixes, with end of life planned no sooner than 1 October 2026. Releases were still appearing in September 2026, but you should plan the upgrade to Express 5.
Which Node.js versions does Fastify 5 support?
Fastify 5 requires Node.js 20 or later. Its LTS page lists Node.js 20 and 22; we also ran it on Node.js 24.19.0 without problems.
Is Fastify faster than Express?
Fastify is designed for lower per-request overhead, and response schemas speed up JSON serialisation. In most real APIs, database and network time dominate, so measure your own routes before choosing on speed.
Do I still need express-async-errors with Express 5?
No. Express 5 forwards rejected promises from handlers and middleware to your error-handling middleware.
Can I use Express middleware in Fastify?
Not natively. Use the equivalent Fastify plugin where one exists, and check each dependency before porting an Express app.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Migrating to Express 5 — Express
- Express@5.1.0: Now the Default on npm with LTS Timeline — Express, 2025-03-31
- Express version support — Express
- Fastify V5 migration guide — Fastify
- Fastify Long Term Support — Fastify
- Fastify validation and serialization — Fastify
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
More from Cloudsoft Zone

Industry updateNode.js
Which Node.js version to use in October 2026: LTS lines and the new schedule
Node.js 26 becomes LTS on 28 October 2026, Node.js 24 moves to maintenance and 20.x is already end of life. Here is every supported line, the new yearly release model from Node.js 27, and which version to run.
By Cloudsoft Zone

TutorialNode.js
How to test a Node.js API with the built-in test runner (node:test)
Write, run, mock and measure coverage for a small Node.js HTTP API using only node:test and node:assert. Covers port-0 servers, fetch(), spies, t.mock.method(), fake dates and coverage thresholds.
By Cloudsoft Zone

ComparisonNode.js
Running TypeScript in Node.js: built-in type stripping vs tsx vs tsc
Node.js now runs .ts files directly by stripping types. Compare built-in type stripping with tsx and a full tsc build: what each supports, what breaks (enums, decorators, paths), and which to pick in 2026.
By Cloudsoft Zone
