Express fundamentals — routing and the middleware pattern

Every piece of Express's convenience — route parameters, automatic body parsing, chained handlers — maps directly onto the raw http module concepts from the previous lesson; nothing here is a fundamentally new mechanism, just a much more declarative way to express the same thing.

Intermediate

3 min read

Route matching: declarative, with real parameter extraction

const express = require("express");
const app = express();
 
app.get("/users/:id", (req, res) => {
  res.json({ id: req.params.id }); // :id automatically extracted, no manual string-parsing needed
});
 
app.listen(3000);

Compare this directly to the previous lesson's manual if (req.method === "GET" && req.url === ...) chain: app.get("/users/:id", ...) declares the method and path pattern together, and Express automatically matches incoming requests against it and extracts the :id segment into req.params.id — the exact string-matching-and-parsing work the raw http module required writing by hand, now handled declaratively. This is Express's core value: the same underlying method + url matching, expressed far more concisely, with real pattern-matching (:id, wildcards) built in.

express.json(): the middleware that gives you req.body for free

app.use(express.json()); // parses a JSON request body automatically, BEFORE your route handlers run
 
app.post("/users", (req, res) => {
  console.log(req.body); // { name: "Ada" } — already parsed, no manual chunk-collecting needed
  res.status(201).json({ created: true });
});

Recall the raw http lesson's manual req.on("data", ...) / req.on("end", ...) chunk collection to build up a request body by hand — express.json() is a middleware (covered in depth in the next lesson) that does exactly that work automatically, for every route, parsing the accumulated body as JSON and attaching the result to req.body before any route handler runs. This is the single most common source of "why doesn't req.body exist" confusion for anyone new to Express: without explicitly registering express.json() via app.use(), req.body is simply undefined.

Route parameters vs query strings: two genuinely different mechanisms

app.get("/users/:id", (req, res) => {
  console.log(req.params.id);  // from the URL PATH itself: /users/42 → "42"
  console.log(req.query.active); // from the QUERY STRING: /users/42?active=true → "true" (always a STRING)
});

req.params comes from named segments in the route pattern (:id), matched against the actual URL path — a required part of the URL structure. req.query comes from everything after the ? in the URL, parsed as key-value pairs — genuinely optional, and every value arrives as a string regardless of what it "looks like" (?active=true gives the string "true", not the boolean true, and ?count=5 gives "5", not the number 5 — a real, common source of bugs when a comparison like req.query.count === 5 silently always fails).

Grouping routes with express.Router()

// routes/users.js
const router = express.Router();
router.get("/", (req, res) => { /* GET /users */ });
router.get("/:id", (req, res) => { /* GET /users/:id */ });
module.exports = router;
 
// app.js
app.use("/users", require("./routes/users")); // mounts the router under a common PREFIX

express.Router() creates a self-contained, mountable set of routes — genuinely useful once an app has more than a handful of endpoints, since it lets related routes (everything under /users) live in their own file, with the common /users prefix specified once at the mount point rather than repeated in every individual route definition. This is a real, structural organization tool, not just a stylistic preference, and it's the standard pattern for any Express app beyond a trivial size.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What does `app.get('/users/:id', ...)` do that the raw http module's manual matching doesn't provide automatically?

2. Why does req.body end up undefined if express.json() isn't registered with app.use()?

3. What's the key difference between req.params and req.query?