Node.js and Express course menu
Express Routing: Routes, Parameters and express.Router
Organise Express routes: HTTP methods for CRUD, route parameters and multiple parameters, optional segments and wildcards in Express 5, route order, app.route for chaining, and splitting routes into modules with express.Router.
On this page
Routing decides which code handles each request, based on its method and path.
Methods#
Each HTTP method has a matching function: app.get, app.post, app.put, app.patch, app.delete, plus app.all for every method. In a REST API they map to actions on resources:
Parameters#
A path segment starting with : is a route parameter. A route can have several:
import express from "express";
const app = express();
app.get("/api/users/:userId/orders/:orderId", (req, res) => {
res.json(req.params);
});
// Optional segment in braces: matches /api/reports and /api/reports/2026
app.get("/api/reports{/:year}", (req, res) => {
res.json({ year: req.params.year ?? "all years" });
});
// Wildcard: *path captures the rest of the URL as an array of segments
app.get("/files/*path", (req, res) => {
res.json({ segments: req.params.path, joined: req.params.path.join("/") });
});
app.listen(3000);curl -s localhost:3000/api/users/42/orders/1001
curl -s localhost:3000/api/reports
curl -s localhost:3000/api/reports/2026
curl -s localhost:3000/files/invoices/2026/oct.pdf{"userId":"42","orderId":"1001"}
{"year":"all years"}
{"year":"2026"}
{"segments":["invoices","2026","oct.pdf"],"joined":"invoices/2026/oct.pdf"}The optional-segment and named-wildcard syntax is from Express 5. Express 4 used :year? and a bare *; you will see those in older tutorials, and they no longer work in Express 5.
Route order matters#
Express checks routes in the order they were added and uses the first match. Put specific paths before general ones:
import express from "express";
const app = express();
app.get("/api/products/featured", (req, res) => res.json({ route: "featured list" }));
app.get("/api/products/:id", (req, res) => res.json({ route: "single product", id: req.params.id }));
app.listen(3000);curl -s localhost:3000/api/products/featured
curl -s localhost:3000/api/products/15{"route":"featured list"}
{"route":"single product","id":"15"}If /api/products/:id came first, featured would be treated as an id.
Chaining with app.route#
app.route(path) groups the handlers for one path:
app.route("/api/books/:id")
.get((req, res) => { /* read */ })
.put((req, res) => { /* replace */ })
.delete((req, res) => { /* delete */ });express.Router: routes in separate files#
As an API grows, put each resource's routes in its own module with express.Router(), then mount it at a path:
import { Router } from "express";
const router = Router();
const books = [
{ id: 1, title: "Godan", author: "Premchand" },
{ id: 2, title: "Malgudi Days", author: "R. K. Narayan" },
];
router.get("/", (req, res) => res.json(books));
router.get("/:id", (req, res) => {
const book = books.find((b) => b.id === Number(req.params.id));
if (!book) return res.status(404).json({ error: "Book not found" });
res.json(book);
});
router.delete("/:id", (req, res) => {
const index = books.findIndex((b) => b.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: "Book not found" });
books.splice(index, 1);
res.status(204).end();
});
export default router;import express from "express";
import booksRouter from "./routes/books.js";
const app = express();
app.use("/api/books", booksRouter); // every path in the router starts with /api/books
app.listen(3000);curl -s localhost:3000/api/books
curl -s localhost:3000/api/books/2
curl -s -X DELETE -o /dev/null -w "%{http_code}\n" localhost:3000/api/books/1
curl -s localhost:3000/api/books
curl -s localhost:3000/api/books/1[{"id":1,"title":"Godan","author":"Premchand"},{"id":2,"title":"Malgudi Days","author":"R. K. Narayan"}]
{"id":2,"title":"Malgudi Days","author":"R. K. Narayan"}
204
[{"id":2,"title":"Malgudi Days","author":"R. K. Narayan"}]
{"error":"Book not found"}Inside the router, paths are relative to where it is mounted: router.get("/:id") handles /api/books/:id. 204 No Content is the usual response to a successful delete, with no body.
Common mistakes#
Practice#
Practice exercise
Create a router for /api/cities with GET / returning all cities and GET /:name returning one city by name, case-insensitively, or a 404 error. Mount it in an app and test GET /api/cities/PUNE.
Show hint
Compare city.name.toLowerCase() === req.params.name.toLowerCase().
Show solution
import { Router } from "express";
const router = Router();
const cities = [
{ name: "Pune", state: "Maharashtra" },
{ name: "Mysuru", state: "Karnataka" },
];
router.get("/", (req, res) => res.json(cities));
router.get("/:name", (req, res) => {
const city = cities.find((c) => c.name.toLowerCase() === req.params.name.toLowerCase());
if (!city) return res.status(404).json({ error: "City not found" });
res.json(city);
});
export default router;import express from "express";
import citiesRouter from "./routes/cities.js";
const app = express();
app.use("/api/cities", citiesRouter);
app.listen(3000);curl -s localhost:3000/api/cities/PUNE
curl -s localhost:3000/api/cities/Delhi{"name":"Pune","state":"Maharashtra"}
{"error":"City not found"}Key takeaway#
Next: Middleware.
Finished this lesson?