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:

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:

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:

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

  1. Pin first. Commit a lockfile and use npm ci in Docker and CI. Choose ^4.21.2 or ^5.1.0 deliberately rather than letting a rebuild decide.
  2. 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/
    
  3. Rewrite with this table:
    • '*' becomes '/{*splat}'.
    • '/x/*' becomes '/x/*rest', then update the handler to req.params.rest.join('/').
    • '/:id?' becomes '{/:id}'.
    • '/:id(\\d+)' becomes '/:id' plus validation in the handler, or a regex literal passed to app.get(/^\/(\d+)$/, ...).
  4. Try the official codemod. npx @expressjs/codemod upgrade handles several mechanical renames. Review its diff, and still grep for wildcards.
  5. 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.
  6. Watch the root path. The most common silent regression is a fallback written as /*splat that 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

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.