The Path Syntax of Express 5

Express 5 24,430 matches paths with path-to-regexp 8, a deliberate break with the version Express 4 used. The old syntax let regular-expression operators leak into path strings, where /ab?cd meant "the b is optional" and /* meant "anything". Those readings were a frequent source of accidental catch-alls and of regular-expression denial-of-service reports. Version 8 reserves ? + * ( ) [ ] ! and throws a PathError the moment you register a pattern that uses them the old way — at startup, not at request time.

Registering Express 4 patterns against Express 5JavaScript
import express from 'express';
for (const path of ['/files/*', '/books/:id?', '/:id(\\d+)', '/(user|users)/:id']) {
  try {
    express().get(path, (req, res) => res.end('ok'));
    console.log('OK    ', path);
  } catch (err) {
    console.log('THROWS', path, '->', err.constructor.name + ': ' + err.message.split(';')[0]);
  }
}
Output
THROWS /files/* -> PathError: Missing parameter name at index 8: /files/*
THROWS /books/:id? -> PathError: Unexpected ? at index 10: /books/:id?
THROWS /:id(\d+) -> PathError: Unexpected ( at index 4: /:id(\d+)
THROWS /(user|users)/:id -> PathError: Unexpected ( at index 1: /(user|users)/:id

Three replacements cover almost every Express 4 pattern. A wildcard must be named, so * becomes *splat (any name works) and captures an array of segments, not a string — /files/img/icons/logo.png matched by /files/*filepath yields ['img', 'icons', 'logo.png'], and req.params.filepath.join('/') rebuilds it. Optional parts go in braces: {/:id} makes the whole segment optional, {.:ext} makes a suffix optional. Alternation is gone, but app.get() still accepts an array of paths, which reads better anyway.

Rewriting the Express 4 path patterns that Express 5 rejects
Express 4 Express 5 Matches
/files/* /files/*splat /files/a/b, not /files
/* /{*splat} every path, root included
/books/:id? /books{/:id} /books and /books/7
/:file.:ext? /:file{.:ext} /logo.png and /logo
/:id(\d+) /:id plus a check any segment; validate in code
/(user|users)/:id ['/user/:id', '/users/:id'] both prefixes

Two traps hide in that list. /files/* throws, but /ab*cd does not — *cd parses as a wildcard named cd, so the route silently matches far more than you meant. And /*splat excludes the root path, so a catch-all that must also answer / needs the braced form /{*splat}. When a pattern genuinely needs character-class precision, pass a real RegExp instead of a string; its capture groups arrive as req.params[0], req.params[1] and so on.