Routing

Node.js · Lesson 14 of 30 7 min read Updated

Routing decides which code runs for each request. A route is a combination of an HTTP method (GET, POST and so on), a path pattern (/books/:id) and a handler function. A small app may have five routes; a real one may have a hundred, split across many files.

In this lesson you will read values from the URL with route parameters, make parts of a path optional, match many levels with wildcards, read query strings, group routes with app.route() and split them into files with express.Router(). Express 5 uses a new, stricter path syntax, so watch for the differences from older tutorials.

Route methods and paths

Each HTTP method has a matching function on the app: app.get(), app.post(), app.put(), app.patch() and app.delete(). app.all() matches every method. Express checks routes in the order you add them and runs the first one that matches.

Paths are matched exactly, except that a trailing slash is ignored by default: /menu and /menu/ both match app.get("/menu", ...). Matching is not case-sensitive by default, so /Menu matches too.

Route parameters

A path segment that starts with : is a route parameter. It matches any value in that position, and Express puts the value in req.params. Here is a demo app that shows every pattern in this lesson:

JavaScript
// app.js
import express from "express";
import trainsRouter from "./routes/trains.js";

const app = express();

// 1. Route parameters
app.get("/students/:roll/marks/:subject", (req, res) => {
  res.json(req.params);
});

// 2. Fixed paths must come before a parameter that could match them
app.get("/books/new", (req, res) => res.send("Form to add a new book"));
app.get("/books/:id", (req, res) => res.send(`Book number ${req.params.id}`));

// 3. Optional part in braces
app.get("/report{/:year}", (req, res) => {
  res.send(`Report for ${req.params.year ?? "the current year"}`);
});

// 4. Named wildcard: matches any number of path segments
app.get("/docs/*path", (req, res) => {
  res.json({ path: req.params.path });
});

// 5. Query strings
app.get("/search", (req, res) => {
  res.json(req.query);
});

// 6. One path, several methods
app.route("/cart")
  .get((req, res) => res.send("Show cart"))
  .post((req, res) => res.send("Add to cart"))
  .delete((req, res) => res.send("Empty cart"));

// 7. A router mounted under a prefix
app.use("/api/trains", trainsRouter);

app.listen(3000, () => console.log("Routing demo on http://localhost:3000"));

Sending a few requests to it with fetch() prints:

Output
GET /students/42/marks/maths -> 200 {"roll":"42","subject":"maths"}
GET /books/new -> 200 Form to add a new book
GET /books/7 -> 200 Book number 7
GET /report -> 200 Report for the current year
GET /report/2025 -> 200 Report for 2025
GET /docs/node/fs/promises -> 200 {"path":["node","fs","promises"]}
GET /search?q=kurta&size=M&color=red&color=blue -> 200 {"q":"kurta","size":"M","color":["red","blue"]}
GET /search?price[max]=999 -> 200 {"price[max]":"999"}
DELETE /cart -> 200 Empty cart
GET /api/trains/12951 -> 200 {"number":"12951","name":"Mumbai Rajdhani","from":"MMCT","to":"NDLS"}
GET /api/trains/99999 -> 404 {"error":"No train 99999"}

Let us go through each pattern.

1. Parameters are always strings. req.params.roll is "42", not 42. Convert with Number() when you need a number, and check the result, because /students/abc/... also matches. Express 5 does not support regular expressions inside paths, so checking values belongs in your handler or in validation (see Validating Input).

2. Order matters. If /books/:id came first, a request for /books/new would match it with id set to "new". Put fixed paths before parameter paths.

3. Optional segments go inside braces. "/report{/:year}" matches both /report and /report/2025. When the part is missing, the parameter is simply absent, so use ?? for a default.

4. Wildcards match one or more path segments. In Express 5 a wildcard needs a name, such as *path, and its value is an array of the segments. "/docs/*path" does not match /docs itself; write "/docs{/*path}" if you want that too.

Query strings

Everything after ? in the URL ends up in req.query. Use query strings for optional settings such as search words, filters, sorting and page numbers, and route parameters for identifying a thing (/books/7).

  • Values are strings: req.query.page is "2", not 2.
  • A key that appears more than once becomes an array, like color above. A key that appears once is a plain string. Code that expects an array must handle both.
  • A missing key is undefined, so give defaults: const page = Number(req.query.page ?? 1).
  • Express 5 uses the simple query parser by default, so price[max]=999 stays a flat key named "price[max]". Express 4 used the "extended" parser, which turned it into a nested object. You can switch back with app.set("query parser", "extended"), but flat keys are safer and easier to validate.
  • req.query is read-only in Express 5. Store cleaned values in a new variable instead of assigning to req.query.

app.route(): one path, many methods

app.route("/cart") lets you attach handlers for several methods to one path without repeating it, as in pattern 6. It keeps related code together and avoids typing mistakes in the path.

express.Router(): routes in separate files

As an app grows, put each group of routes in its own file. A router is a mini app that holds routes; you mount it on the main app under a path prefix:

JavaScript
// routes/trains.js
import { Router } from "express";

const router = Router();

const trains = [
  { number: "12015", name: "Ajmer Shatabdi", from: "NDLS", to: "AII" },
  { number: "12951", name: "Mumbai Rajdhani", from: "MMCT", to: "NDLS" },
];

router.get("/", (req, res) => {
  res.json(trains);
});

router.get("/:number", (req, res) => {
  const train = trains.find((t) => t.number === req.params.number);
  if (!train) return res.status(404).json({ error: `No train ${req.params.number}` });
  res.json(train);
});

export default router;

Inside the router, paths are written relative to the mount point. Because app.js mounts it with app.use("/api/trains", trainsRouter), the router's "/" answers /api/trains and "/:number" answers /api/trains/12951. A typical project layout looks like this:

Text
shop-api/
  app.js
  routes/
    products.js    -> mounted at /api/products
    orders.js      -> mounted at /api/orders
    users.js       -> mounted at /api/users

Notice the return before res.status(404)... in the router. It stops the function after sending the error, so the code does not try to send a second response.

Common mistakes

  • Putting /books/:id before /books/new. The parameter route catches new. Order fixed paths first.
  • Treating parameters and query values as numbers. They are strings; convert and check them.
  • Writing the full path inside a router. If the router is mounted at /api/trains, write router.get("/:number"), not router.get("/api/trains/:number").
  • Using Express 4 wildcards. "*" throws Missing parameter name in Express 5; give the wildcard a name.
  • Forgetting return after sending a response in an if block, which leads to ERR_HTTP_HEADERS_SENT.

Key points

  • A route is a method plus a path pattern plus a handler; Express runs the first route that matches, in the order added.
  • :name captures one segment into req.params; values are strings.
  • Braces make a part optional ("/report{/:year}"); named wildcards ("/*path") capture many segments as an array.
  • req.query holds the query string; repeated keys become arrays, and Express 5 uses the simple parser.
  • app.route() groups methods for one path; express.Router() splits routes into files mounted with app.use(prefix, router).

Exercise

Build a router file routes/cities.js for a weather API, mounted at /api/cities. It should have GET / (list three cities with a temperature), GET /:name (one city, case-insensitive, or 404), and GET /:name/forecast{/:days} that returns the city name and the number of days (default 3). Mount it in app.js.

Show solution
JavaScript
// routes/cities.js
import { Router } from "express";

const router = Router();
const cities = [
  { name: "Jaipur", temp: 31 },
  { name: "Kochi", temp: 29 },
  { name: "Lucknow", temp: 27 },
];

const findCity = (name) => cities.find((c) => c.name.toLowerCase() === name.toLowerCase());

router.get("/", (req, res) => res.json(cities));

router.get("/:name", (req, res) => {
  const city = findCity(req.params.name);
  if (!city) return res.status(404).json({ error: "City not found" });
  res.json(city);
});

router.get("/:name/forecast{/:days}", (req, res) => {
  const city = findCity(req.params.name);
  if (!city) return res.status(404).json({ error: "City not found" });
  res.json({ city: city.name, days: Number(req.params.days ?? 3) });
});

export default router;
JavaScript
// app.js
import express from "express";
import citiesRouter from "./routes/cities.js";

const app = express();
app.use("/api/cities", citiesRouter);
app.listen(3000);
Output
GET /api/cities/kochi -> 200 {"name":"Kochi","temp":29}
GET /api/cities/jaipur/forecast -> 200 {"city":"Jaipur","days":3}
GET /api/cities/jaipur/forecast/7 -> 200 {"city":"Jaipur","days":7}
GET /api/cities/surat -> 404 {"error":"City not found"}

Comparing lowercase names makes the lookup case-insensitive, and ?? gives the default of 3 days when the optional segment is missing.

Quick Quiz

1. For the route "/orders/:id", what is req.params.id when the URL is /orders/105?

2. Which Express 5 path matches both /report and /report/2025?

3. A router is mounted with app.use("/api/users", usersRouter). Which router path answers GET /api/users/7?

4. With Express 5 defaults, what is req.query.tag for ?tag=js&tag=node?

Learn Node.js with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call