Express 5 is the first major version update in nearly a decade. It brings modernized internals, a new version of path-to-regexp, and several breaking changes that affect most Express applications. Here's what changed and how Ovvoc automates the migration.
What changed in Express 5
Express 5 is largely backwards-compatible, but a few critical changes will break most applications if not addressed:
path-to-regexpupgraded from v1 to v8 (wildcard and parameter syntax)- Several deprecated methods removed
@types/express@5changes parameter types- Promise rejection handling in middleware
Wildcard syntax: * → {*path}
The most impactful change. Express 5 uses path-to-regexp v8, which requires wildcards to be named parameters. The bare * wildcard no longer works.
// Catch-all routeapp.get('/files/*', serveFiles);// API wildcardapp.use('/api/*', apiRouter);// Static fallbackapp.get('*', (req, res) => { res.sendFile('index.html');});// Catch-all route+ app.get('/files/{*path}', serveFiles);// API wildcard+ app.use('/api/{*path}', apiRouter);// Static fallback+ app.get('{*path}', (req, res) => { res.sendFile('index.html');});Optional parameters: :param? → {/:param}
Optional route parameters also use new syntax in path-to-regexp v8:
// Optional format parameterapp.get('/users/:id.:format?', getUser);// Optional language prefixapp.get('/:lang?/about', aboutPage);// Optional format parameter+ app.get('/users/:id{.:format}', getUser);// Optional language prefix+ app.get('{/:lang}/about', aboutPage);Deprecated methods
Express 5 removes several methods that were deprecated in Express 4:
res.json(status, obj)→res.status(status).json(obj)res.send(status, body)→res.status(status).send(body)res.sendfile()→res.sendFile()(capital F)req.param()→req.params,req.body, orreq.query
@types/express@5 changes
For TypeScript projects, @types/express@5 changes the type of req.params values from string to string | string[]. This breaks every place where you access route parameters without type narrowing:
// req.params.id is stringconst id = req.params.id;const user = await db.findUser(id);// req.params.id is string | string[]+ const id = Array.isArray(req.params.id)+ ? req.params.id[0]+ : req.params.id;const user = await db.findUser(id);How Ovvoc automates all 8 transforms
Ovvoc's Express 4→5 migration applies 8 distinct AST transforms:
- Wildcard
*→{*path}in route strings - Optional
:param?→{/:param}in route strings res.json(status, obj)→res.status(status).json(obj)res.send(status, body)→res.status(status).send(body)res.sendfile()→res.sendFile()req.param('name')→req.params.name- Version bump in
package.json - Lockfile regeneration
Each transform is applied using AST analysis — not string replacement. The full project is then built and tested in an isolated container. Only if everything passes does Ovvoc open a PR.
Full example: before and after
const express = require('express');const app = express();app.get('/api/users/:id', getUser);app.get('/api/users/:id.:format?', getUserFormatted);app.use('/api/*', apiMiddleware);app.get('*', (req, res) => { res.sendfile('public/index.html');});app.listen(3000);const express = require('express');const app = express();app.get('/api/users/:id', getUser);+ app.get('/api/users/:id{.:format}', getUserFormatted);+ app.use('/api/{*path}', apiMiddleware);+ app.get('{*path}', (req, res) => {+ res.sendFile('public/index.html');});app.listen(3000);The migration is fully automated, fully tested, and delivered as a single clean PR.
Why Express 5 exists
Express 5 is the first major version update after nearly a decade of Express 4 stability. The primary motivation is modernizing the routing engine by upgrading topath-to-regexp v8, which brings stricter, more predictable route matching. Express 5 also removes methods that were deprecated in Express 4, improves async error handling so that rejected promises in middleware are automatically caught, and drops support for Node.js versions below 18.
The long gap between major versions means that Express 4 accumulated a significant number of deprecated APIs that couldn't be removed without a breaking change. Express 5 is primarily a cleanup release — it modernizes the internals without fundamentally changing how you build Express applications. If your Express 4 code doesn't use deprecated methods or bare wildcards, much of it works unchanged on Express 5.
Complete breaking changes list
Beyond the wildcard and optional parameter changes covered above, Express 5 includes several additional breaking changes that may affect your application:
req.hostreturns hostname without port — in Express 4,req.hostincluded the port number (e.g.,localhost:3000). In Express 5, it returns only the hostname (localhost). Usereq.hostnamefor the same behavior, orreq.headers.hostfor the full host headerreq.queryparser changes — the default query string parser behavior has been tightened. Complex nested query strings may parse differently. Test your query parameter handling thoroughlyres.redirect()no longer accepts relative URLs by default — relative redirects likeres.redirect('back')still work, but relative path redirects require explicit configurationapp.del()removed — theapp.del()alias forapp.delete()was deprecated in Express 4 and is removed in Express 5. Useapp.delete()insteadreq.param()removed — this method searchedreq.params,req.body, andreq.queryin that order. It's removed in Express 5 — access each source directlybody-parserno longer bundled — Express 5 does not include body-parser by default. If you relied on the implicit inclusion, you need to install and configure it explicitlypath-to-regexpwildcards — bare*must become{*path}with a named parameter- Optional params syntax change —
:param?must become{/:param}using the new brace syntax
Testing your migration
A thorough testing strategy is critical for validating your Express 5 migration. Here's the recommended approach:
- Run your existing test suite against Express 5 — this is the fastest way to identify breaking changes that affect your application. Most well-tested Express apps will show failures immediately for deprecated method usage and route pattern changes
- Add integration tests for all routes — if you don't already have integration tests that exercise every route, add them before migrating. Tools like
supertestmake this straightforward. Hit every route with expected parameters and verify the response - Test middleware ordering — Express 5 handles middleware errors differently. Verify that your error-handling middleware still catches errors correctly, especially if you rely on middleware ordering for error propagation
- Verify error handling behavior — Express 5 automatically catches rejected promises in async middleware. If your middleware relies on manual
next(err)calls for async errors, test that the automatic handling doesn't change your error response format - Check
req.paramstypes in TypeScript projects — with@types/express@5, every access toreq.paramsvalues needs type narrowing. Runtsc --noEmitto find all type errors before deploying
TypeScript considerations
The @types/express@5 package introduces a significant typing change:req.params values change from string to string | string[]. This reflects the fact that path-to-regexp v8 can produce array values for repeated parameters and wildcards.
In practice, this means every route handler that accesses req.params needs type narrowing. The simplest approach is an inline type assertion:
const id = req.params.id as string;
For a safer approach, create a typed route helper that extracts and validates parameters:
function getParam(params: Record<string, string | string[]>, key: string): string
This helper can check for array values, handle edge cases, and provide a single point of control for parameter extraction across your entire application. If you have dozens of route handlers, a centralized helper is far more maintainable than scattering type assertions throughout your codebase.
Middleware compatibility
Most popular Express middleware packages work with Express 5 without changes, but some require version updates:
Works out of the box:
cors— fully compatible with Express 5helmet— fully compatible, all security headers work as expectedcompression— fully compatible, no API changesmorgan— fully compatible, logging formats unchangedcookie-parser— fully compatible, cookie handling unchanged
May need version updates:
express-validator— check that your version supports Express 5. Older versions may not handle the newreq.paramstypes correctlypassport— version 0.7 or later is required for Express 5 compatibility. Earlier versions relied on deprecated Express APIsmulter— test file upload handling with Express 5. Core functionality works, but edge cases around error handling may differ
Known issues:
express-session— older versions may not work with Express 5. Update to the latest version before migrating
Performance notes
Express 5 is not primarily a performance release, but there are some measurable differences worth noting:
path-to-regexp v8 is faster for static routes. The new version compiles route patterns more efficiently, resulting in faster matching for routes without parameters. For applications with many static routes (like API endpoints with fixed paths), this translates to marginally faster request routing.
Dynamic route performance is similar. Routes with parameters, wildcards, and optional segments perform comparably to Express 4. The new syntax is different but the underlying matching performance is not significantly changed.
Overall request handling is unchanged. Express 5 doesn't change the core middleware pipeline architecture. Response times, throughput, and memory usage are essentially the same as Express 4 for identical workloads.
The primary benefit of migrating to Express 5 is maintainability, not performance. You gain access to modern APIs, automatic async error handling, and a codebase that's aligned with the active maintenance branch of Express. Performance improvements, if any, are a bonus — not the reason to migrate.