Routing
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:
// 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:
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.pageis"2", not2. - A key that appears more than once becomes an array, like
colorabove. 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]=999stays a flat key named"price[max]". Express 4 used the "extended" parser, which turned it into a nested object. You can switch back withapp.set("query parser", "extended"), but flat keys are safer and easier to validate. req.queryis read-only in Express 5. Store cleaned values in a new variable instead of assigning toreq.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:
// 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:
shop-api/
app.js
routes/
products.js -> mounted at /api/products
orders.js -> mounted at /api/orders
users.js -> mounted at /api/usersNotice 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/:idbefore/books/new. The parameter route catchesnew. 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, writerouter.get("/:number"), notrouter.get("/api/trains/:number"). - Using Express 4 wildcards.
"*"throwsMissing parameter namein Express 5; give the wildcard a name. - Forgetting
returnafter sending a response in anifblock, which leads toERR_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.
:namecaptures one segment intoreq.params; values are strings.- Braces make a part optional (
"/report{/:year}"); named wildcards ("/*path") capture many segments as an array. req.queryholds 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 withapp.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
// 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;// app.js
import express from "express";
import citiesRouter from "./routes/cities.js";
const app = express();
app.use("/api/cities", citiesRouter);
app.listen(3000);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?
Route parameters are always strings; convert them yourself.
2. Which Express 5 path matches both /report and /report/2025?
Optional parts go inside braces in Express 5; the ? suffix is Express 4 syntax.
3. A router is mounted with app.use("/api/users", usersRouter). Which router path answers GET /api/users/7?
Router paths are relative to the mount prefix.
4. With Express 5 defaults, what is req.query.tag for ?tag=js&tag=node?
A repeated query key becomes an array of strings.
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
