TypeError: Missing parameter name at 1 in Express: Fix Express 5 Routes
Node.js · Intermediate · 6 min read · published
This article was written by Claude (Anthropic) and published automatically.
What this solves: Your Express app crashes on startup with 'Missing parameter name' after npm pulled Express 5. Here is how to rewrite wildcard, optional and regex routes for it.
What Changed
If your app now dies at startup with TypeError: Missing parameter name at 1: https://git.new/pathToRegexpError, you are running Express 5. Its router no longer accepts the bare * wildcard and other Express 4 path syntax. Express 5.0 shipped stable in late 2024, and 5.1 became the default latest tag on npm in 2025. A plain npm install express, or a "latest" / "*" range without a lockfile, now quietly hands you a new major version.
The breaking piece is the path matcher. Express 5 uses path-to-regexp v8, which replaced the old string-to-regex approach with a strict parser:
- Wildcards must be named. Write
/*splat, not*or/*. - Wildcard params come back as an array.
req.params.splatfor/a/b/cis['a','b','c'], not the string'a/b/c'. - Optional segments use braces. Write
/user{/:id}, not/user/:id?. - No inline regex.
/:id(\\d+)is rejected. - Reserved characters.
( ) [ ] ? + !are reserved and must be escaped with\\if you mean them literally.
The number in the message is the character index where the parser gave up. at 1 almost always means a route that is just *.
The Old Way vs The New Way
Express 4 (path-to-regexp 0.1.x):
app.get('/api/users/:id(\\d+)', getUser); // inline regex
app.get('/docs/:page?', docs); // optional param
app.get('/assets/*', (req, res) => {
const rest = req.params[0]; // 'img/logo.png'
res.sendFile(rest, { root: 'public' });
});
app.all('*', notFoundOrSpa); // catch-all
Express 5 (path-to-regexp v8):
app.get('/api/users/:id', (req, res, next) => { // validate in code
if (!/^\d+$/.test(req.params.id)) return next('route');
return getUser(req, res, next);
});
app.get('/docs{/:page}', docs); // braces = optional
app.get('/assets/*file', (req, res) => {
const rest = req.params.file.join('/'); // array -> 'img/logo.png'
res.sendFile(rest, { root: 'public' });
});
app.all('/{*splat}', notFoundOrSpa); // also matches '/'
Why It Was Added
The old matcher turned your path string into a regular expression with very little checking. Two routes with parameters in the same segment, such as /:a-:b, could compile into a regex that backtracks catastrophically on crafted input. That was CVE-2024-45296, a ReDoS: one request could pin a CPU core.
The loose syntax also hid bugs. Unnamed * captures landed in req.params[0], and nobody remembered what that index meant. Modifiers like ?, + and * behaved differently depending on where they appeared. And a path containing a literal ( would silently become a capture group.
v8 swaps that flexibility for a small, unambiguous grammar that only compiles to safe regexes. Anything ambiguous now fails at startup instead of misrouting in production.
How It Works Underneath
Path strings are tokenized and compiled once, when you call app.get() or router.use(), not on each request. That is why the error kills the process at boot, with a stack trace through router/lib/layer.js instead of your handler.
flowchart TD
A["app.get('/files{/*path}', h)"] --> B[Router creates Layer]
B --> C[path-to-regexp v8 lexer]
C --> D{Token after '*' or ':' ?}
D -- "missing name" --> E["throw TypeError: Missing parameter name at N"]
D -- "reserved char ( ? + [" --> F["throw TypeError: Unexpected token"]
D -- valid --> G["Tokens: text '/files', group {text '/', wildcard 'path'}"]
G --> H["Compile to non-backtracking RegExp"]
H --> I[Request /files/a/b]
I --> J["Match: wildcard splits on '/'"]
J --> K["req.params.path = ['a','b']"]
K --> L[handler h runs]
Key mechanics:
:namematches exactly one segment. It never crosses a/.*namematches one or more segments. It needs at least one character, so/*splatdoes not match/. Wrapping it as{*splat}makes it optional.{...}is an optional group. It can hold text and params together, as in/docs{/:page}or/file{.:ext}.- Decoding happens per segment. Wildcards return arrays so an encoded
%2Finside one segment stays distinct from a real separator.
Express 5 also changed async error handling: a rejected promise from a handler is passed to next(err) automatically. That is unrelated to routing, but it is the other behaviour people notice during migration.
Should You Adopt It Yet
Yes, for new projects. Express 5 is stable, it is the npm default, and Express 4 is in maintenance. Starting a new service on 4 means a forced migration later.
For existing apps, it is cheap if your routes are mostly plain. If you only use /:param routes, the upgrade is close to a no-op. It gets more expensive when:
- you rely on inline regex routes (
/:id(\\d+)); - your code reads
req.params[0]; - third-party middleware registers its own
*routes internally; - you depend on other removed APIs, such as
res.send(200),app.del,req.param()or string-path redirects to'back'.
Wait if a critical plugin hasn't published Express 5 support. A route library registering '*' will crash your app at boot even if your own code is clean.
Migration Notes
- Pin first. Commit a lockfile and use
npm ciin Docker and CI. Choose^4.21.2or^5.1.0deliberately rather than letting a rebuild decide. - Grep for the patterns that break:
grep -rnE "(get|post|put|patch|delete|all|use)\(\s*['\"\`][^'\"\`]*(\*|:[a-zA-Z_]+\?|\(|\+)" src/ grep -rn "req.params\[0\]" src/ - Rewrite with this table:
'*'becomes'/{*splat}'.'/x/*'becomes'/x/*rest', then update the handler toreq.params.rest.join('/').'/:id?'becomes'{/:id}'.'/:id(\\d+)'becomes'/:id'plus validation in the handler, or a regex literal passed toapp.get(/^\/(\d+)$/, ...).
- Try the official codemod.
npx @expressjs/codemod upgradehandles several mechanical renames. Review its diff, and still grep for wildcards. - Boot the app in CI. Because path errors are thrown at registration, a test that only imports the app and calls
listen(0)catches every bad route before deploy. - Watch the root path. The most common silent regression is a fallback written as
/*splatthat no longer serves/. Use/{*splat}when the root should match too.
Key takeaway: In Express 5, every wildcard needs a name (`/*splat`), optional segments use braces (`{/:id}`), and a catch-all that should also match the root must be written `/{*splat}`.
Real-world challenge
A React SPA is served by a small Express server. CI rebuilt the Docker image with no code changes and the container now exits immediately. The logs show `TypeError: Missing parameter name at 1: https://git.new/pathToRegexpError`, and the stack trace runs through `router/lib/layer.js`. The package.json lists `"express": "latest"` and the Dockerfile runs `npm install` without a lockfile. The only route that isn't a plain `/api/...` path is `app.get('*', (req, res) => res.sendFile(indexHtml))`.
Diagnosis
- The crash happens at boot, not on a request. Express 5 compiles route paths with path-to-regexp v8 when
app.get()is called, and the error is thrown right there. "latest"with no lockfile means the rebuild pulled Express 5.x. Express 5 became npm'slatesttag in 2025.- A bare
*is the culprit. In v8,*starts a named wildcard, and the parser hits the end of the string at index 1 without finding a name.
Fix
Name the wildcard. Use the brace form so / is also served by the fallback:
// Express 5
app.get('/{*splat}', (req, res) => res.sendFile(indexHtml));
Then stop the drift. Pin "express": "^5.1.0" (or ^4.21.2 if you are not ready to migrate), commit package-lock.json, and switch the Dockerfile to npm ci. A framework major version should never arrive through a rebuild nobody asked for.
Finally, check that your /api routes are registered before the fallback. Otherwise unknown API paths return index.html with a 200 status instead of a 404.