What Is New in Express 5
| Category | Change |
|---|---|
| New Features | Async/await promise rejection now automatically forwarded to error-handling middleware; Brotli compression support in body parser; customizable URL-encoded body depth limit |
| Improvements | Updated path-to-regexp to v8; Node.js 18+ only (drops legacy version baggage); simplified optional/wildcard route syntax; urlencoded parser defaults extended to false |
| Bug Fixes | CVE-2024-45590 mitigated via URL-encoded body depth cap (default 32); ReDoS attack surface eliminated by removing inline regex in routes |
| Breaking Changes | Node.js <18 no longer supported; inline route regex removed; unnamed capture groups removed; deprecated res.* method signatures removed; app.param(fn) removed; req.body no longer defaults to {}; bodyParser() combination middleware removed |
| Deprecations Removed | res.redirect('back'), res.send(status), res.json(status, obj), res.sendfile, app.del(), req.acceptsCharset/Encoding/Language (singular forms), app.param(fn) |
Why does Express 5 drop old Node.js versions and what does that unlock?
Express 5 requires Node.js 18 or later, and that single requirement change is what makes the rest of the release possible. Supporting Node.js 0.10 through 16 meant the team could not adopt modern runtime APIs, could not drop polyfill dependencies, and CI was a mess of version matrices that nobody enjoyed maintaining.
In practice, dropping those old versions means the project can now use native Promise, async/await, and newer V8 engine features without shims. It also means fewer transitive dependencies and a smaller attack surface on the supply chain side. If your production servers are still on Node.js 16 or below, this is your real migration blocker -- everything else is fixable in an afternoon.
For teams with genuinely stuck applications, the HeroDevs "never-ending support" partnership covers critical security patches on v4 after it reaches end-of-life, so you have a runway. But the correct long-term move is upgrading Node.js first, then Express.
How does route path matching change in Express 5 and will my existing routes break?
Express 5 swaps path-to-regexp from v0.1.x to v8, and the gap between those two versions is the most likely source of breakage in a real migration. Here is a concise before/after of the patterns that changed:
| Express v4 Pattern | Express v5 Equivalent | Notes |
|---|---|---|
/:id? |
{/:id} |
Optional segments now use curly braces |
* |
*splat |
Wildcards must be named |
/:foo(\d+) |
Use a validation library | Inline regex removed entirely (ReDoS risk) |
/user(s?) => req.params[0] |
Named parameter required | Numeric params gone; all params must be named |
The characters (, ), [, ], ?, +, and ! are now reserved in route strings. If any of your routes contain these characters for reasons other than the old optional/group syntax, they will throw or match unexpectedly. Audit your router files before upgrading.
The removal of inline regex is the security-motivated change here -- it closes the ReDoS attack vector where a crafted URL could send a route-matching loop into exponential backtracking. The recommended replacement is a validation library applied inside the handler, which keeps that logic testable and explicit anyway.
Does Express 5 finally handle async middleware errors automatically?
Yes -- rejected promises from async middleware are now caught by the router and treated as next(err) calls, which is the behavior most developers already expected but had to wire up manually in v4.
What this looks like in practice
In Express v4 you had to wrap every async call or the unhandled rejection would crash the process (or get swallowed silently, depending on your Node.js version):
// Express v4 -- manual try/catch required
app.use(async (req, res, next) => {
try {
req.locals.user = await getUser(req);
next();
} catch (err) {
next(err);
}
});
In Express v5, the router wraps your async handler and catches rejected promises for you:
// Express v5 -- rejection forwarded automatically
app.use(async (req, res, next) => {
req.locals.user = await getUser(req); // throws => next(err) called automatically
next();
});
Watch out for one nuance: this only covers rejected promises. A resolved promise does not automatically call next() -- you still do that yourself. If getUser resolves but you forget to call next(), the request will hang just like it did in v4. The async error safety net is real; automatic flow control is not.
What deprecated API signatures were finally cut in Express 5?
Express 5 removes a long list of method signatures that were marked deprecated in v3 and v4 -- most of them argument-order variants that existed for historical reasons and caused more confusion than they solved.
The most common ones you will hit during migration:
res.send(status, body)andres.send(body, status)-- useres.status(status).send(body)res.send(status)where status is a number -- useres.sendStatus(status)res.json(status, obj)-- useres.status(status).json(obj)res.jsonp(status, obj)-- useres.status(status).jsonp(obj)res.redirect(url, status)-- argument order flipped; useres.redirect(status, url)res.redirect('back')andres.location('back')-- usereq.get('Referrer') || '/'explicitlyres.sendfile()-- useres.sendFile()(capital F)app.del('/', fn)-- useapp.delete('/', fn)app.param(fn)-- access params viareq.params,req.body, orreq.querydirectlyreq.acceptsCharset,req.acceptsEncoding,req.acceptsLanguage-- add the pluralsto each
Most teams will find these in a grep of their codebase rather than at runtime, because these patterns tend to show up in older controller or route files that do not have test coverage. Run grep -rn "res.send(" src/ and similar before you upgrade so you are not debugging 500s in production.
The bodyParser() combination convenience middleware is also removed. If you were importing body-parser and calling it as a single function that set up both JSON and URL-encoded parsing in one shot, split those into two explicit app.use() calls. The individual parsers are bundled in Express 5 itself, so you may not even need the separate package anymore.
Common Questions about Express 5
Can I upgrade to Express 5 without upgrading Node.js first?
No. Express 5 requires Node.js 18 or later, so upgrading Node.js is a prerequisite, not an option you can defer.
Does Express 5 automatically call next() when an async middleware resolves successfully?
No -- resolved promises do not automatically advance the middleware chain. You still call next() yourself after your async work completes. Only rejected promises are caught automatically and forwarded as next(err).
How do I replace inline route regex like /:id(\d+) in Express 5?
Move the validation into the handler or a dedicated middleware using a library such as joi, zod, or express-validator. For example: define the route as /:id, then in the handler check that req.params.id matches /^\d+$/ and call next with a 400 error if it does not. This approach is more testable than inline regex and eliminates the ReDoS risk entirely.
Is req.body still initialized to an empty object by default in Express 5?
No. In Express 5, req.body is undefined until a body-parsing middleware runs, so guard any access to it with a check rather than assuming it is always an object.
What is the correct way to redirect to the referring page in Express 5?
Use res.redirect(req.get('Referrer') || '/') -- the magic string 'back' is no longer accepted by res.redirect() or res.location().
Do I still need the body-parser npm package with Express 5?
For most use cases, no. Express 5 bundles the JSON and URL-encoded body parsers directly, so you can call express.json() and express.urlencoded() without installing body-parser separately. The standalone package is still available if you need its extended configuration options.