Skip to content
Express.js ex routing 4 min read

Route Paths & Patterns

The first argument to a route method like app.get() is a path — the pattern Express compares against the incoming request URL to decide whether a handler runs. That path can be a literal string, a string with special pattern characters, or a full JavaScript regular expression. Understanding how each form matches (and how Express 5 changed the rules) is the difference between routes that fire exactly when you expect and confusing 404s. This page walks through all three styles and the gotchas that come with the Express 5 path-matching engine.

String paths

The simplest and most common form is an exact string. Express matches the request path against it literally, ignoring the query string.

const express = require("express");
const app = express();

app.get("/", (req, res) => res.send("home"));
app.get("/about", (req, res) => res.send("about page"));
app.get("/users/profile", (req, res) => res.send("profile"));

app.listen(3000);

A request to GET /about?ref=nav still matches /about — the ?ref=nav part is parsed into req.query and never participates in path matching. Trailing slashes matter only if you do not enable strict routing; by default /about and /about/ are treated the same.

Output:

GET /about        -> about page
GET /about/       -> about page
GET /about?ref=x  -> about page
GET /About        -> 404 (matching is case-sensitive off by default, but path is)

Pattern characters in string paths

Express lets you embed a small set of pattern characters directly in a string path. These operate on the character or group immediately before them, much like in a regular expression. They are convenient for collapsing several similar URLs into one route.

CharacterMeaningExample pathMatches
?Preceding char is optional/colou?r/color, /colour
+Preceding char one or more times/ab+c/abc, /abbc, /abbbc
*Wildcard (zero or more of any char)/files/*/files/a, /files/a/b.txt
()Group for the operators above/ab(cd)?e/abe, /abcde
// "/colour" and "/color" both work
app.get("/colou?r", (req, res) => res.send("color route"));

// one or more "b"
app.get("/ab+c", (req, res) => res.send("abc route"));

// group made optional
app.get("/ab(cd)?e", (req, res) => res.send("grouped route"));

Note: These characters are path operators, not literal characters. If you genuinely need to match a literal + or * in a URL, escape it or rely on a route parameter instead.

Wildcards and Express 5

The * wildcard is where Express 4 and Express 5 differ the most. In Express 4, an unnamed * matched any sequence of characters and you read the captured value from req.params[0]:

// Express 4 style
app.get("/files/*", (req, res) => {
  res.send(`requested: ${req.params[0]}`); // for /files/img/a.png -> "img/a.png"
});

Express 5 upgraded its internal path-to-regexp to a newer major version that removed the bare * and the (.*) regex-in-string forms. Wildcards must now be named, written as *name, and the captured value is an array of path segments rather than a single string:

// Express 5 style — wildcards must be named
app.get("/files/*filepath", (req, res) => {
  // req.params.filepath is an array, e.g. ["img", "a.png"]
  res.send(`requested: ${req.params.filepath.join("/")}`);
});

Output:

GET /files/img/a.png -> requested: img/a.png

Optional segments also changed: the old :param? syntax is replaced by braces, {/:param}, and the regex character classes you could once inline in a string path are no longer allowed. If a route worked in Express 4 but throws a path-to-regexp error after upgrading, an unnamed * or inline regex is almost always the cause.

BehaviorExpress 4Express 5
Bare wildcard* allowedMust be named: *name
Wildcard capturereq.params[0] (string)req.params.name (array)
Optional param/:id?/users{/:id}
Inline regex in string/user/(\\d+) allowedRemoved — use a RegExp route

Regular expression routes

For matching that pattern characters cannot express, pass a real JavaScript RegExp as the path. The regex is tested against the request path, and capture groups land in req.params by index.

// Match any path ending in ".json" or ".xml"
app.get(/.*\.(json|xml)$/, (req, res) => {
  res.send(`format: ${req.params[0]}`); // "json" or "xml"
});

// Match a numeric-only id segment
app.get(/^\/products\/(\d+)$/, (req, res) => {
  res.json({ id: req.params[0] });
});

Output:

GET /products/42   -> {"id":"42"}
GET /products/abc  -> 404 (regex rejects non-digits)
GET /data.json     -> format: json

Regex routes are the right tool when you need strict validation (digits only, a fixed file extension, a versioned API prefix like /v[12]/) directly at the routing layer instead of inside the handler. Because they are opaque, document them well — a stray ^ or $ quietly changes what matches.

Best Practices

  • Prefer plain string paths and named route parameters first; reach for patterns only when a single route genuinely needs to cover several URLs.
  • On Express 5, always name your wildcards (*splat) and remember the capture is an array — join it before using it as a path.
  • Anchor regex routes with ^ and $ so they match the whole path and do not accidentally catch substrings.
  • Keep heavy validation logic out of regex paths when a parameter plus a check inside the handler reads more clearly to your team.
  • When migrating from Express 4, grep your routes for bare *, :param?, and inline (\\d+) patterns — these are the forms the new matcher rejects.
  • Register specific routes before broad wildcard routes, since Express matches in registration order and the first match wins.
Last updated June 14, 2026
Was this helpful?