Skip to content
Guides

Express 4 to 5 Migration: What Changes and How Ovvoc Handles It

Ovvoc team··7 min read

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-regexp upgraded from v1 to v8 (wildcard and parameter syntax)
  • Several deprecated methods removed
  • @types/express@5 changes 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.

Express 4
// Catch-all route
app.get('/files/*', serveFiles);
// API wildcard
app.use('/api/*', apiRouter);
// Static fallback
app.get('*', (req, res) => {
res.sendFile('index.html');
});
Express 5
// 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:

Express 4
// Optional format parameter
app.get('/users/:id.:format?', getUser);
// Optional language prefix
app.get('/:lang?/about', aboutPage);
Express 5
// 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, or req.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:

Before (@types/express@4)
// req.params.id is string
const id = req.params.id;
const user = await db.findUser(id);
After (@types/express@5)
// 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:

  1. Wildcard *{*path} in route strings
  2. Optional :param?{/:param} in route strings
  3. res.json(status, obj)res.status(status).json(obj)
  4. res.send(status, body)res.status(status).send(body)
  5. res.sendfile()res.sendFile()
  6. req.param('name')req.params.name
  7. Version bump in package.json
  8. 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

app.js (Express 4)
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);
app.js (Express 5)
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:

  1. req.host returns hostname without port — in Express 4, req.host included the port number (e.g., localhost:3000). In Express 5, it returns only the hostname (localhost). Use req.hostname for the same behavior, or req.headers.host for the full host header
  2. req.query parser changes — the default query string parser behavior has been tightened. Complex nested query strings may parse differently. Test your query parameter handling thoroughly
  3. res.redirect() no longer accepts relative URLs by default — relative redirects like res.redirect('back') still work, but relative path redirects require explicit configuration
  4. app.del() removed — the app.del() alias for app.delete() was deprecated in Express 4 and is removed in Express 5. Use app.delete() instead
  5. req.param() removed — this method searched req.params, req.body, and req.query in that order. It's removed in Express 5 — access each source directly
  6. body-parser no 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 explicitly
  7. path-to-regexp wildcards — bare * must become {*path} with a named parameter
  8. 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:

  1. 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
  2. Add integration tests for all routes — if you don't already have integration tests that exercise every route, add them before migrating. Tools like supertest make this straightforward. Hit every route with expected parameters and verify the response
  3. 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
  4. 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
  5. Check req.params types in TypeScript projects — with @types/express@5, every access to req.params values needs type narrowing. Run tsc --noEmit to 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 5
  • helmet — fully compatible, all security headers work as expected
  • compression — fully compatible, no API changes
  • morgan — fully compatible, logging formats unchanged
  • cookie-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 new req.params types correctly
  • passport — version 0.7 or later is required for Express 5 compatibility. Earlier versions relied on deprecated Express APIs
  • multer — 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.

Stay up to date

Automate your dependency updates. Start with one repo.