Without notes, state yesterday’s main idea and one unresolved question.
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.
- Create the project exactly as above and confirm
npm run typecheckexits without errors. - Write
src/server.tswith/health,/equipment, and/equipment/:id. Start it withnpm run dev. - Verify each route with curl and record the status code for each.
- Verify
GET /equipment/eq-999returns404with a message naming the id. - Verify
GET /nopereturns404with the route-level message. - Add a third record to the array.
tsx watchrestarts on save; confirmGET /equipmentshows three records without you restarting anything. - Add
GET /equipment/:id/serialreturning{ "serial": "..." }for a known id and404otherwise. Confirm both cases. - 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
returnafter sending a response. The handler carries on and tries to respond twice. Express logsError [ERR_HTTP_HEADERS_SENT]. Return immediately after everyres.json. - Registering the catch-all first. Every request 404s. The stack runs top to bottom.
- Expecting numbers in
req.params. They are strings. Convert withNumber(...)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.tsout of habit. Usenpm run devsotsxhandles TypeScript and restarts on save.
Verify it yourself
Open today's reference, Express's Basic routing, and compare it with what you built.
- Express's docs list route methods this lesson did not use. Name two and say what HTTP method each corresponds to.
- Find Express's own description of a route path and a route parameter. Does it agree that parameter values arrive as strings?
- Express documents
app.all(). Read what it does and write one sentence on when it would be better thanapp.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
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Create an Express TypeScript project with health, equipment list, and equipment detail routes.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
An API with clear routes and tested 404 behavior.
Working with AI today
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
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.