0 / 91
Week 6 · Day 39 of 91

Express routing

HTTP, Node.js, and APIs

Objective

Use a minimal framework while retaining the HTTP mental model.

HTTP is a request/response protocol like a defined communication bus: method, address, headers, body, and status all have roles.

  • app and middleware
  • route handlers
  • path parameters

Why this matters

Your Day 38 server worked, but look at what it cost: an if for every route, both method and path checked by hand, JSON.stringify and headers written manually, and no way at all to answer /equipment/eq-002 without string-slicing req.url yourself.

Express is a thin layer over node:http that removes exactly that busywork. Today you rebuild the same server in Express, in TypeScript, and add a route with a path parameter — the piece that turns "a list" into "a resource you can address". Nothing new happens at the HTTP level. Every request still arrives as a method and a path, and every response still ends with a status code and a body; Express only changes how you write the code in between.

Setting up the project

Make a fresh project. This is the codebase you will extend for the rest of the week.

mkdir -p ~/fullstack-journey/equipment-api && cd ~/fullstack-journey/equipment-api
npm init -y
npm install express
npm install -D typescript tsx @types/express @types/node

express is the framework. typescript is the compiler from Week 5. tsx runs a TypeScript file directly, no build step, and can restart on save. @types/express and @types/node are the type definitions that let TypeScript check your Express and Node code (Day 31's types, supplied by the package rather than written by you).

Edit package.json so it contains these fields — keep the versions npm installed:

{
  "name": "equipment-api",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "typecheck": "tsc --noEmit"
  }
}

"type": "module" is yesterday's ES-modules switch. Add tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

"noEmit": true says TypeScript only checks; tsx does the running. So npm run typecheck is your design-rule check and npm run dev is your server.

The app and the middleware stack

An Express application is a value you create and then configure:

import express from "express";
const app = express();

app holds an ordered list of functions called the middleware stack. When a request arrives, Express walks that list from top to bottom, offering the request to each entry until one sends a response. Each entry has a signature you already recognise from Day 38, plus a third argument:

(req, res, next) => { /* ... */ }

req and res are Express's enriched versions of Node's request and response. next is a function meaning "I am not handling this — carry on down the list". A middleware either responds or calls next(). If it does neither, the request hangs, exactly as a missing res.end() did yesterday.

app.use(fn) adds a function that runs for every request. app.get(path, fn), app.post(path, fn) and friends add functions that run only when the method and path both match.

Order matters, always. A catch-all registered before your routes will swallow every request.

Signal chain, not a lookup table

The middleware stack is a signal chain: the request enters at the top and passes through each stage in the order you wired them. A stage can inspect the signal, modify it, pass it on, or terminate the chain by driving the output. Put a terminator early in the chain and nothing downstream ever sees a signal — which is precisely why the 404 fallback goes last. Express is not a routing table it searches; it is series-connected stages it walks.

A row of desks

A visitor walks past a row of desks in order, asking each one their question. Any desk can answer and send them away, or wave them along to the next. A desk staffed by someone who answers everything ends the journey, so where you place that desk in the row decides who ever gets past it. That is why the catch-all goes at the end of the row and not the start.

Route handlers

A route handler is the function that maps one method plus one path to your code.

app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

res.json(value) does three things Day 38 made you do by hand: runs JSON.stringify, sets Content-Type: application/json, and ends the response. The default status is 200; to change it, chain res.status(404).json({ ... }). res.status() alone sends nothing — it only sets the number.

Express also gives you res.send() for text and res.end() for an empty body. For an API, use res.json() for anything with content.

Path parameters

A path segment written with a leading colon is a path parameter: a named slot that matches whatever appears there.

app.get("/equipment/:id", (req, res) => {
  // GET /equipment/eq-002  →  req.params.id === "eq-002"
});

Express collects the matched values into req.params. :id is a name you choose. Values in req.params are always strings/equipment/7 gives you "7", not 7 — because a URL is text.

Use a path parameter when the value identifies which thing you mean. Use a query string (?status=down, Day 41) when it modifies how you want the collection returned. /equipment/eq-002 names one machine; /equipment?status=down filters many.

The data still lives in a plain array in memory, and still disappears when the process restarts. Databases are Week 7; today's job is the HTTP surface.

Walkthrough: three routes and a 404

Create src/server.ts:

import express from "express";
import type { Request, Response } from "express";

const app = express();
const PORT = 3000;

type Equipment = {
  id: string;
  name: string;
  serial: string;
  status: "operational" | "maintenance" | "down";
};

const equipment: Equipment[] = [
  { id: "eq-001", name: "Feed Pump 3", serial: "FP3-2019-114", status: "operational" },
  { id: "eq-002", name: "Cooling Fan A", serial: "CFA-2021-008", status: "down" }
];

app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

app.get("/equipment", (req, res) => {
  res.json(equipment);
});

app.get("/equipment/:id", (req, res) => {
  const match = equipment.find((item) => item.id === req.params.id);
  if (!match) {
    res.status(404).json({ error: `No equipment with id "${req.params.id}"` });
    return;
  }
  res.json(match);
});

app.use((req: Request, res: Response) => {
  res.status(404).json({ error: `No route for ${req.method} ${req.originalUrl}` });
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});

Two 404s, on purpose, saying different things. The one inside /equipment/:id means "the path is a real route, the id is not a real record". The app.use at the bottom means "there is no such route at all". Both are client errors; a caller reading the message can tell which mistake they made.

Notice the return after res.status(404).json(...). Without it, execution continues to res.json(match) and Express warns that headers were already sent.

npm run typecheck
npm run dev
Listening on http://localhost:3000

In a second terminal:

curl -s localhost:3000/equipment/eq-002
curl -i -s localhost:3000/equipment/eq-999 | head -1
curl -s localhost:3000/nope
{"id":"eq-002","name":"Cooling Fan A","serial":"CFA-2021-008","status":"down"}
HTTP/1.1 404 Not Found
{"error":"No route for GET /nope"}

Prove the order rule

Move the app.use catch-all above the app.get routes and save. tsx watch restarts automatically. Now every request returns {"error":"No route for GET /health"} — the terminator is first in the chain. Move it back.

Checkpoint

Say what app.get, res.json, res.status, and req.params each do, and explain in one sentence why the catch-all must be registered last.

Your turn

Build the deliverable: an API with clear routes and tested 404 behaviour.

  1. Create the project exactly as above and confirm npm run typecheck exits without errors.
  2. Write src/server.ts with /health, /equipment, and /equipment/:id. Start it with npm run dev.
  3. Verify each route with curl and record the status code for each.
  4. Verify GET /equipment/eq-999 returns 404 with a message naming the id.
  5. Verify GET /nope returns 404 with the route-level message.
  6. Add a third record to the array. tsx watch restarts on save; confirm GET /equipment shows three records without you restarting anything.
  7. Add GET /equipment/:id/serial returning { "serial": "..." } for a known id and 404 otherwise. Confirm both cases.
  8. Write an endpoint table in README.md: method, path, what it returns, and every status code it can produce. You will extend this table every day this week.

Reviewer mode — once every route works

"Check my routes for ambiguity, inconsistent naming, and missing not-found responses." A review must produce specific findings with evidence: this path, this case, this missing status. Reject general praise, and reject a wholesale rewrite. Verify each finding with curl before you act on it — a reviewer that cannot show you the failing request has not shown you a defect.

Common pitfalls

  • Forgetting return after sending a response. The handler carries on and tries to respond twice. Express logs Error [ERR_HTTP_HEADERS_SENT]. Return immediately after every res.json.
  • Registering the catch-all first. Every request 404s. The stack runs top to bottom.
  • Expecting numbers in req.params. They are strings. Convert with Number(...) and check the result before using it.
  • res.status(404) on its own. It sets the status but sends nothing, so the request hangs. Always chain .json(...) or .send(...).
  • Running node src/server.ts out of habit. Use npm run dev so tsx handles TypeScript and restarts on save.

Verify it yourself

Open today's reference, Express's Basic routing, and compare it with what you built.

  1. Express's docs list route methods this lesson did not use. Name two and say what HTTP method each corresponds to.
  2. Find Express's own description of a route path and a route parameter. Does it agree that parameter values arrive as strings?
  3. Express documents app.all(). Read what it does and write one sentence on when it would be better than app.get.

Add the answers to README.md under a "Notes" heading. Tomorrow you accept your first request body, and the validation problem becomes real.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Create an Express TypeScript project with health, equipment list, and equipment detail routes.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

An API with clear routes and tested 404 behavior.

Working with AI today

AI as skeptical reviewer

Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.

Check my routes for ambiguity, inconsistent naming, and missing not-found responses.

References

End-of-day quiz

Q1 What maps a method and path to code in Express?
Q2 Which result best proves today’s work is complete?
Q3 What should an AI code review primarily produce?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.