Express 5.0.0 24,430 shipped on 10 September 2024, a decade after the branch opened, and 5.1.0 took the latest tag on npm 2,036 in March 2025. The 4.x line still ships under latest-4 (4.22.3) with security fixes, but new work starts on 5. Most Express 4 code runs unchanged; the breaks are in path syntax and old signatures.
| Express 4 | Express 5 | Why |
|---|---|---|
| app.get('/files/*', h) | app.get('/files/*splat', h) | Wildcards must be named |
| app.get('/:file.:ext?', h) | app.get('/:file{.:ext}', h) | Optional parts use braces |
| npm i body-parser | express.json() built in | Parsers moved into core |
| res.json(obj, 201) | res.status(201).json(obj) | Two-argument forms removed |
| .catch(next) required | rejections auto-forwarded | Async handlers just work |
Two more: req.query is now a read-only getter, so code that stashed a normalized value onto it throws, and req.body is undefined rather than {} when no parser ran. The path syntax bites first, though, because path-to-regexp 8 refuses ambiguous patterns at startup: a bare '/files/*' throws Missing parameter name at index 8, and '/books/:id?' throws Unexpected ? at index 10. A named wildcard also behaves differently — req.params.splat is an array of segments, so /files/covers/2026/ab.png gives {"splat":["covers","2026","ab.png"]}. The Path Syntax of Express 5 has the full syntax.
The other change to internalize is automatic rejection forwarding. In Express 4 an async handler that threw left the request hanging until the socket timed out, because the router never saw the rejection. Express 5 awaits the handler's return value and passes a rejection to next, so the error thrown by app.get('/boom', async () => { throw new Error('unreachable'); }) lands in the error middleware as a 500.