ExpressJS

Fix “TypeError: Missing parameter name” After Upgrading to Express 5

a person holding a paper
On this page
  1. Find the route that breaks
  2. Fix 1: Name your wildcards
  3. Fix 2: Update catch-all routes
  4. Fix 3: Rewrite optional parameters
  5. Fix 4: Replace regex characters with an array
  6. Fix 5: Remove app.options(‘*’)
  7. Characters that now need escaping
  8. Why Express changed the route syntax
  9. A complete before-and-after routes file
  10. When the error comes from a package, not your code
  11. Test every route after the upgrade
  12. Quick reference
  13. Rolling the fix out safely on a large project
  14. Frequently asked questions
  15. Oh hi there 👋It’s nice to meet you.
  16. Sign up to receive awesome content in your inbox.

You upgraded to Express 5 and the app crashes before it even starts:

TypeError: Missing parameter name at 1: https://git.new/pathToRegexpError

The exact position and wording vary, and you may also see “Unexpected ( at …” or “Unexpected ? at …”. All of them come from path-to-regexp, the library Express uses to read route strings. Express 5 ships a newer, stricter version, so route patterns that worked in Express 4 are now rejected.

Find the route that breaks

The stack trace shows which file registered the route. Search your project for route strings with *, ?, ( or ):

grep -rnE "\.(get|post|put|patch|delete|all|use|options)\(['\"][^'\"]*[*?()]" --include=*.js src/

Don’t forget routes registered by packages, such as old middleware or proxy setups. If the trace points into node_modules, update that package.

Fix 1: Name your wildcards

A wildcard now needs a name, and its value arrives in req.params as an array of path segments:

// Express 4
app.get('/files/*', handler);

// Express 5
app.get('/files/*splat', (req, res) => {
  const filePath = req.params.splat.join('/'); // ['docs', 'a.pdf'] becomes 'docs/a.pdf'
  res.send(filePath);
});

Note that /files/*splat does not match /files itself. To match both, make the wildcard optional with braces: /files{/*splat}.

Fix 2: Update catch-all routes

The classic single-page app fallback is the most common cause of this error:

// Express 4
app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'dist', 'index.html')));

// Express 5: matches every path, including /
app.get('/{*splat}', (req, res) => res.sendFile(path.join(__dirname, 'dist', 'index.html')));

For a 404 handler at the end of your routes, you don’t need a path at all:

app.use((req, res) => {
  res.status(404).json({ message: 'Not found' });
});

Fix 3: Rewrite optional parameters

The ? suffix is gone. Wrap the optional part in braces instead:

Express 4 Express 5
'/users/:id?' '/users{/:id}'
'/:file.:ext?' '/:file{.:ext}'
'/posts/:slug/:page?' '/posts/:slug{/:page}'

Fix 4: Replace regex characters with an array

Route strings can no longer contain regular-expression groups. Use an array of plain paths:

// Express 4
app.get('/(about|team)', handler);

// Express 5
app.get(['/about', '/team'], handler);

If you really need a regular expression, pass a RegExp object instead of a string: app.get(/^\/v[12]\/status$/, handler).

Fix 5: Remove app.options(‘*’)

Many CORS tutorials include app.options('*', cors()). In Express 5 it throws the same error. Delete it: app.use(cors()) already answers preflight requests. See how to fix CORS errors in a MERN app for a complete setup.

Characters that now need escaping

If a literal path contains ( ) [ ] ? + !, escape it with a backslash in the route string, for example '/price\\(usd\\)' in JavaScript source. Most apps never need this.

Upgrading the whole app? Follow the full Express 5 migration guide.

Why Express changed the route syntax

The old path-to-regexp versions turned route strings into regular expressions in ways that were hard to predict. A pattern such as /:id? or a string containing ( and ) could produce a regular expression that matched more than you meant, and some patterns could be abused with long, specially crafted URLs to make the regular expression run for a very long time. That kind of attack is called ReDoS (regular expression denial of service), and it can freeze a Node.js server because the event loop is stuck evaluating one request.

The new version removes the ambiguous parts. Every special piece of a route is now explicit: a named parameter starts with :, a wildcard starts with * and has a name, and anything optional sits inside braces. Routes become slightly more verbose but much easier to read. When you see /users{/:id}, you know exactly which part is optional, which wasn’t always true with the old question-mark syntax.

A complete before-and-after routes file

Here is a realistic Express 4 file with every pattern that breaks in Express 5. Converting a whole file at once is easier than fixing errors one by one, because the server stops at the first bad route it finds.

// routes.js (Express 4)
router.get('/users/:id?', listOrGetUser);
router.get('/files/*', serveFile);
router.get('/docs/:page.:format?', renderDoc);
router.get('/(about|team|careers)', renderStaticPage);
router.all('*', notFound);
// routes.js (Express 5)
router.get('/users{/:id}', listOrGetUser);
router.get('/files/*splat', serveFile);
router.get('/docs/:page{.:format}', renderDoc);
router.get(['/about', '/team', '/careers'], renderStaticPage);
router.use(notFound); // no path needed for a catch-all 404

Check each handler after converting, because the values in req.params can change shape. The wildcard is now an array, so code that did req.params[0] in Express 4 must read req.params.splat and usually join it:

function serveFile(req, res) {
  // Express 4: const relative = req.params[0];
  const relative = req.params.splat.join('/');
  const safe = path.normalize(relative).replace(/^(\.\.[/\\])+/, '');
  res.sendFile(path.join(__dirname, 'public', safe));
}

The path.normalize line is worth keeping in any file-serving route. It stops requests such as /files/../../.env from escaping the folder you intended to serve.

When the error comes from a package, not your code

Sometimes the stack trace points into node_modules. That means a library registered a route with old syntax when you called it. The usual suspects are:

  • Older proxy setups that register a catch-all route for you. Update http-proxy-middleware and similar packages to their latest major version.
  • Documentation and admin middleware that mounts its own routes, such as API docs viewers. Most have released Express 5 compatible versions.
  • Copy-pasted helpers inside your own utils folder that call app.options('*', …) or app.get('*', …).

Run npm outdated to see which packages have newer versions, update them, and read each package’s changelog for an “Express 5” note. If a package is abandoned and still registers old patterns, replace it, or wrap your own route around the functionality it provided.

Test every route after the upgrade

A route that no longer matches doesn’t crash the server; it quietly returns 404. Automated tests catch this before your users do. With supertest you can check the routes you changed in a few lines:

// routes.test.js  (run with: node --test)
const test = require('node:test');
const assert = require('node:assert');
const request = require('supertest');
const app = require('./app');

test('optional id route matches both forms', async () => {
  assert.equal((await request(app).get('/users')).status, 200);
  assert.equal((await request(app).get('/users/42')).status, 200);
});

test('wildcard route receives the full path', async () => {
  const res = await request(app).get('/files/docs/guide.pdf');
  assert.notEqual(res.status, 404);
});

test('unknown paths return the JSON 404', async () => {
  const res = await request(app).get('/does-not-exist');
  assert.equal(res.status, 404);
});

Export your Express app from one file and call app.listen() from another, so tests can load the app without opening a real port.

Quick reference

You want to match Express 5 pattern Example URL req.params
One required segment /users/:id /users/42 { id: '42' }
An optional segment /users{/:id} /users {}
Everything after a prefix /files/*splat /files/a/b.png { splat: ['a', 'b.png'] }
A prefix and everything after it /files{/*splat} /files {}
Every path /{*splat} /anything/here { splat: ['anything', 'here'] }
Several fixed paths ['/a', '/b'] /b {}
A custom pattern /^\/v[12]\/status$/ (a RegExp object) /v2/status Capture groups by index

You can choose any name for a wildcard. splat is just the convention used in the Express documentation; /files/*path works the same way and gives you req.params.path.

Rolling the fix out safely on a large project

On a small app you can fix every route in one sitting. On a larger codebase with dozens of route files, a calmer process avoids surprises in production:

  1. Collect every route first. Add a temporary script that loads your app and prints each registered path, or simply run the grep command shown earlier and save the output to a file. That list becomes your checklist.
  2. Convert by pattern, not by file. Fix all wildcards, then all optional parameters, then all regular-expression strings. Doing one kind of change at a time makes code review much easier, because each diff looks the same.
  3. Keep the old and new behavior side by side in tests. For each converted route, write one test for the URL that used to match and one for a URL that shouldn’t. Optional parameters and trailing slashes are where behavior changes most often.
  4. Deploy to staging and watch the 404s. A sudden rise in 404 responses after the upgrade usually points to a route that no longer matches. Your hosting logs or an error tracker will show which paths are affected.
  5. Ship, then monitor for a day. Keep the previous release ready to roll back. Most teams find any missed route within the first few hours of real traffic.

If you also changed middleware during the upgrade, deploy the route changes and the middleware changes separately when you can. When something breaks, you will know immediately which change caused it.

Frequently asked questions

Why did Express change the route syntax?

The new path-to-regexp version removed patterns that were ambiguous or could cause slow regular expressions, a known security risk. Named wildcards and braces make routes easier to read and safer to match.

What does req.params.splat contain?

An array of the path segments matched by the wildcard. For a request to /files/docs/a.pdf on /files/*splat, it is ['docs', 'a.pdf'].

Can I keep Express 4 instead?

You can for now, but new features and most fixes go into Express 5. The route changes usually take less than an hour, so upgrading is worth it.

Does this error also happen with app.use()?

Yes. app.use() and router.use() paths go through the same parser. A mount path such as app.use('/api/*', handler) must become app.use('/api', handler), since use already matches every path below its mount point.

Why does my optional route now return 404 for the short URL?

The optional part must include the slash: write /users{/:id}, not /users/{:id}. With the second form, /users doesn’t match, but /users/ with a trailing slash does.

logo

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox.

We don’t spam! Read our privacy policy for more info.

Saurabh

Full-stack developer writing practical guides on the MERN stack, AI and DevOps. Every code sample on this site is tested before it is published.