💻 Coding
Express 4 to 5 Migration Planner for Node.js APIs: path-to-regexp 8 Route Syntax for Named Wildcards and Optional Braces, Removed Methods, req.query Getter and Simple Parser, Async Error Handling, Middleware Compatibility, and a Test Plan
Upgrade an Express 4 app to Express 5 route by route: rewrite wildcard, optional, and regex routes for the new path syntax, replace removed methods like app.del, req.param, and res.json with a status argument, handle the read only req.query and the simple query parser, drop try and catch wrappers that async error handling makes redundant, check middleware, and test before release.
0Reviews
Prompt
Act as a Node.js backend engineer who has upgraded production Express 4 APIs to Express 5, reviews route files for a living, and has shipped the bug where a wildcard route silently stopped matching after the path syntax changed.
Inputs:
- Current Express version, Node.js version, and package.json dependencies that touch Express: [Stack]
- Route definitions and router files (paste the app.get, router.use, and similar lines): [Routes]
- Code that uses request and response helpers, especially req.param, req.query writes, res.json with two arguments, res.send with a number, res.redirect, and res.sendfile: [HandlerCode]
- Middleware list in order, including third party packages and any that modify req.query: [Middleware]
- Test setup and how the API is deployed: [Tests]
- Output format: [Format]
Generate:
1. A readiness check: Express 5 needs Node.js 18 or newer, so confirm Stack first; list each dependency in Stack and Middleware with a note on whether its docs state Express 5 support or need checking.
2. A route syntax table for every line in Routes using path-to-regexp 8 rules: wildcards must be named (/files/*splat, and /{*splat} to also match the root), optional parts use braces (/users{/:id}) instead of a trailing question mark, inline regex such as /:id(\d+) is removed and becomes validation in the handler or a router.param check, and reserved characters must be escaped. Note that wildcard params arrive as arrays of segments.
3. A removed and changed API list from HandlerCode, each with before and after code: app.del to app.delete, req.param(name) to req.params, req.body, or req.query, res.json(obj, status) to res.status(status).json(obj), res.send(number) to res.sendStatus, res.sendfile to res.sendFile, res.redirect('back') to the Referrer header with a fallback, and the pluralized req.acceptsCharsets, acceptsEncodings, and acceptsLanguages.
4. Query and body changes: req.query is a getter and cannot be reassigned, the default query parser is now simple, how to opt back in with app.set('query parser', 'extended') if nested objects are used, express.urlencoded defaults to extended false, and req.body is undefined when no body parser ran.
5. Async error handling: rejected promises and thrown errors in async handlers now reach next(err), so list which try and catch wrappers or express-async-errors style packages can be removed, and confirm the error handler still has four arguments.
6. Other behavior changes to check: res.status accepts only integers from 100 to 999, express.static ignores dotfiles by default, and any code that depended on old behavior.
7. A migration order: run the official codemods if available (check the Express migration guide), fix routes, fix handlers, update middleware, then tests.
8. A test plan: one request per changed route including root, nested, optional, and invalid id cases, query parsing tests for nested parameters, and an error path test for an async handler.
Constraints:
- Only rewrite code shown in the Inputs. Never invent routes, packages, or versions.
- Flag anything you cannot confirm as "check the Express 5 migration guide". No em dashes.Instructions
Replace every [bracket] with your details before running. Works on ChatGPT, Claude, and Gemini.
Generated Output
This image was generated using the prompt above.

Examples
Example Input
Stack: express 4.19.2, Node.js 16 in production, body-parser 1.20, express-async-errors 3.1, qs used by a filter endpoint
Routes: app.get('/files/*', serveFile); router.get('/users/:id?', getUser); router.get('/orders/:id(\d+)', getOrder); app.del('/sessions/:sid', logout)
HandlerCode: getUser uses req.param('id'); createOrder does res.json(order, 201); logout ends with res.redirect('back'); health check does res.send(200)
Middleware: helmet, cors, body-parser.urlencoded({ extended: true }), a sanitize middleware that does req.query = clean(req.query)
Tests: Jest with supertest, deployed with Docker on Node.js 16 image
Format: checklist with code diffsExample Output
0) READINESS
- BLOCKER: production runs Node.js 16. Express 5 requires Node.js 18 or newer. Move the Docker base image to a current Node.js LTS first, run the Jest suite on Express 4, then start the upgrade.
- helmet, cors: check each package's docs for Express 5 support.
- express-async-errors: no longer needed after step 5. Remove it.
- body-parser 1.20: Express 5 ships express.json and express.urlencoded; use those.
1) ROUTES
| Express 4 | Express 5 | Note |
| /files/* | /files/*splat | req.params.splat is an array, use req.params.splat.join('/') |
| /users/:id? | /users{/:id} | id is undefined on /users |
| /orders/:id(\d+) | /orders/:id | regex removed, validate below |
| app.del('/sessions/:sid') | app.delete('/sessions/:sid') | app.del removed |
getOrder validation:
+ router.param('id', (req, res, next, id) => /^\d+$/.test(id) ? next() : res.status(404).end())
2) HANDLERS
getUser
- const id = req.param('id')
+ const id = req.params.id
createOrder
- res.json(order, 201)
+ res.status(201).json(order)
logout
- res.redirect('back')
+ res.redirect(req.get('Referrer') || '/')
health check
- res.send(200)
+ res.sendStatus(200)
serveFile
- const rel = req.params[0]
+ const rel = req.params.splat.join('/')
Keep the existing path traversal check on rel.
3) QUERY AND BODY
- sanitize middleware breaks: req.query = clean(req.query) cannot reassign a getter in Express 5.
+ req.cleanQuery = clean(req.query) // update handlers to read req.cleanQuery
- The filter endpoint uses nested qs syntax such as ?filter[status]=open. The default parser is now simple, so add:
+ app.set('query parser', 'extended')
- body-parser.urlencoded({ extended: true })
+ express.urlencoded({ extended: true }) // default is now false, so keep it explicit
4) ASYNC ERRORS
Remove require('express-async-errors'). Rejected promises in async handlers now go to next(err). Confirm the error handler signature stays (err, req, res, next).
5) ORDER OF WORK
1. Node.js upgrade and green tests on Express 4
2. Run the Express codemods if available; check the Express 5 migration guide
3. Routes table above
4. Handler diffs
5. Query, body, and sanitize changes
6. npm i express@5, run tests
6) TESTS (supertest)
[ ] GET /files/a/b/c.pdf returns the file
[ ] GET /users and GET /users/42 both return 200
[ ] GET /orders/abc returns 404, GET /orders/7 returns 200
[ ] DELETE /sessions/x logs out and redirects
[ ] GET /filter?filter[status]=open returns only open items
[ ] An async handler that throws returns the JSON error from the error handler