Express 5 migration: what breaks in your API and how to fix it
The route syntax errors, silent req.query and req.body changes, and async error handling, with a checklist.

You bump express to ^5, run the app, and it crashes before serving a request with Missing parameter name at index 2: /*. Or worse, it starts fine and a pagination middleware silently stops working. This Express 5 migration guide covers the changes that actually break production APIs, how to find each one in your codebase, and the fix.
The direct answer: Express 5 needs Node 18 or newer, changes route path syntax (named wildcards, no ? or regex in strings), makes req.query read-only, leaves req.body undefined when nothing parsed it, validates res.status(), removes old method signatures, and forwards rejected promises to your error handler. Most APIs migrate in an afternoon; the route syntax is where the time goes.
Why migrate to Express 5 now?
Express 5.0 shipped in September 2024 after a decade on 4.x, 5.1 followed in March 2025 and became the latest tag on npm, and the current release is 5.2.1. Express 4 moved to maintenance (security and high-priority fixes only) on April 1, 2025, and the Express team’s proposed schedule puts its end of life no sooner than October 1, 2026. Not an emergency, but the clock is running. The reasons to move are practical: async error handling without wrappers, a stricter path matcher that rejects ambiguous patterns at startup, and new middleware and tutorials targeting 5.
Check your runtime first. Express 5 requires Node 18+, and Node 20 reaches end of life on April 30, 2026. If you are moving anyway, Node 24 LTS is the sensible target.
Step-by-step Express 5 migration plan
1. Upgrade and run the codemods
npm install express@5
npm install --save-dev @types/express@5 # TypeScript only
npx @expressjs/codemod upgrade
The official codemod rewrites the mechanical changes: res.redirect('back'), pluralized req.accepts* methods, the old res.json(obj, status) style signatures and req.param(). It does not fix route paths.
2. Find the route patterns that will throw
Search for patterns path-to-regexp v8 rejects:
grep -rnE "\.(get|post|put|patch|delete|all|use)\(\s*['\"][^'\"]*(\*|\?|\(|\))" src/
Express 5 throws at registration time, so the app will not even start until they are fixed. That is the good news: nothing slips through to production. Here is what failed when registering common Express 4 patterns on 5.2:
THROW /* -> Missing parameter name at index 2: /*
THROW /users/:id? -> Unexpected ? at index 10: /users/:id?
THROW /files/:path(*) -> Unexpected ( at index 12: /files/:path(*)
THROW /api/v(1|2)/x -> Unexpected ( at index 6: /api/v(1|2)/x
3. Rewrite them
| Express 4 | Express 5 |
|---|---|
app.get('/*', ...) |
app.get('/*splat', ...), or '/{*splat}' to also match / |
'/users/:id?' |
'/users{/:id}' |
'/files/:path(*)' |
'/files/*path' |
'/api/v(1|2)/x' |
['/api/v1/x', '/api/v2/x'] |
Wildcard params are now arrays: /*splat on /a/b/c gives req.params.splat as ['a', 'b', 'c']. Code that did req.params[0] needs req.params.splat.join('/'). Optional parameters that are missing are omitted from req.params, not set to undefined.
What breaks at runtime after the Express 5 upgrade
These do not crash at startup, which makes them the dangerous ones.
req.query is now a getter
// Express 4 pattern that silently stops working
app.use((req, res, next) => {
req.query.limit ??= '50'; // lost: req.query is re-parsed on every access
next();
});
In ESM (strict mode), req.query = {...} throws Cannot set property query of #<IncomingMessage> which has only a getter. Mutating a property does not throw; the change just disappears. Put normalized values somewhere you own:
app.use((req, res, next) => {
res.locals.page = {
limit: Math.min(Number(req.query.limit ?? 50), 200),
cursor: req.query.cursor ?? null,
};
next();
});
The default query parser is “simple”
?filter[status]=open used to give { filter: { status: 'open' } }. In Express 5 it gives { 'filter[status]': 'open' }. If your API uses bracket syntax, opt back in with app.set('query parser', 'extended'). Same story for forms: express.urlencoded() now defaults to extended: false.
req.body is undefined, not {}
When no body parser ran (no body, wrong Content-Type), req.body is undefined. Code like const { email } = req.body now throws a TypeError, which becomes a 500. Validate the body, or default it: const { email } = req.body ?? {}.
res.status() validates its argument
res.status('201') throws Invalid status code: "201". Status code must be an integer. Values must be integers from 100 to 999. Watch for status codes read from config, upstream responses or error objects as strings.
Rejected promises go to your error handler
app.get('/v1/orders/:id', async (req, res) => {
const order = await db.orders.find(req.params.id); // a throw here...
res.json(order);
});
app.use((err, req, res, next) => { // ...lands here
req.log?.error({ err }, 'unhandled route error');
res.status(err.status ?? 500).json({ error: 'internal_error' });
});
You can delete express-async-errors and hand-written asyncHandler wrappers. Check that your error handler sets a sensible status, because errors that used to become unhandled rejections now produce responses.
Smaller changes worth a search
app.del()is gone; useapp.delete().res.sendfile()is nowres.sendFile().express.staticignores dotfiles by default; serve/.well-knownexplicitly withdotfiles: 'allow'.req.hostkeeps the port.app.listenpasses errors such asEADDRINUSEto its callback instead of throwing.
Express 5 migration checklist
Forward this to whoever owns the upgrade:
- [ ] Node 18+ everywhere (ideally Node 22 or 24).
- [ ]
npx @expressjs/codemod upgraderun and reviewed. - [ ] App starts: every route pattern registers.
- [ ] No writes to
req.query; bracket queries still parse. - [ ] No unguarded destructuring of
req.body. - [ ] Every
res.status()receives an integer. - [ ] Error handler returns a sane status for async errors.
- [ ] Deployed to staging with real traffic before production.
A solo developer can do this in one sitting. A team of ten should split it: one person on route patterns, one on query and body handling, and a staging soak with production-like traffic before the cutover, ideally after you have graceful shutdown in place so the deploy itself does not drop requests.
FAQ
Is Express 5 backwards compatible with Express 4?
Mostly, but not fully. Most middleware works unchanged, while route path syntax, req.query, req.body defaults and several removed methods need changes. Expect your app to fail at startup until route patterns are updated.
How do I fix “Missing parameter name” in Express 5?
Wildcards must be named. Replace /* with /*splat (or /{*splat} to also match the root) and read the matched segments from req.params.splat, which is an array.
Do I still need express-async-errors in Express 5?
No. Express 5 forwards rejected promises and errors thrown in async handlers to your error middleware, as if you had called next(err).
Doing this with ApiTraffic
The scary part of the Express 5 migration is the runtime changes that do not crash. If you would rather not eyeball logs to find them, ApiTraffic captures every request and response from one middleware placed after your body parsers, and records them under the matched route pattern, so a route that stopped matching shows up as traffic moving to 404.
app.use(express.json());
app.use(apitraffic.express());
After the deploy, search for status_code >= 500 per endpoint, or record a deploy event and use Regressions to compare each endpoint’s p95 before and after the upgrade.
Start free and watch your Express 5 upgrade request by request: apitraffic.io
Sources: Express: Moving to Express 5; Express 5.1.0 release announcement (March 2025); @expressjs/codemod; path-to-regexp README; Node.js release schedule.
Keep reading
Node.js graceful shutdown: stop dropping requests on every deploy
A burst of 502s on every deploy is usually the old process dying with requests in flight. Here is the shutdown order and Express and Fastify code that fixes it in an afternoon.
Zod validation in Express and Fastify: request bodies, query strings and useful errors
A null where your handler expected an array becomes a 500 and a support ticket. Validate bodies, query strings and params with Zod 4 in Express 5 and Fastify 5, return 400s clients can act on, and stop extra fields at the door.
Cursor vs offset pagination: an API design that survives big tables and scrapers
Offset pagination was fast at launch; now deep pages hammer the database and one API key is walking every page. Here is when to switch to cursors and how.