Without notes, state yesterday’s main idea and one unresolved question.
Week 6 API milestone
HTTP, Node.js, and APIs
Objective
Complete and manually verify the in-memory maintenance API.
HTTP is a request/response protocol like a defined communication bus: method, address, headers, body, and status all have roles.
- happy and failure paths
- API documentation
- separation of routes and logic
Why this matters
Your API works. Today you make it something another person could use and another developer could change: you move the business logic out of the route handlers, you exercise every happy path and every failure path deliberately, and you write the two documents that make an API usable — an endpoint table and a runnable request collection.
This is also where the week's honest limitation gets stated out loud. Every record lives in a JavaScript array. Restart the process and it resets. That is the correct place to be at the end of Week 6, and Week 7 replaces the array without changing a single URL — provided you do today's separation properly.
Separating routes from logic
Right now src/server.ts mixes two different jobs. A PATCH handler translates HTTP (read
req.params.id, choose a status code, call res.json) and manipulates data (find the record,
change the field). Those change for different reasons and should live in different files.
The rule: a route handler translates between HTTP and your domain, and nothing else. It reads
the request, calls a plain function, and turns that function's answer into a status code and a body.
The plain function knows nothing about HTTP — no req, no res, no status codes.
The payoff is concrete:
- Testable. You can call
create({ name, serial, status })from a test with no server running and no ports involved. That is Week 11's work, made possible today. - Reusable. The same function serves an HTTP route, a command-line script, and a scheduled job.
- Swappable. In Week 7 the store's internals become database queries. The routes do not change, because they never knew where the data was.
Separate the interface layer from the logic
A well-built instrument does not put the front-panel wiring in the middle of the measurement
circuit. There is a signal-processing block, and there is an interface block that converts between
it and whatever the outside world speaks — buttons, a display, a serial bus. Swap the interface
from RS-232 to USB and the measurement block is untouched, because the boundary was drawn.
equipment-store.ts is the measurement block; the Express routes are the interface converter.
Draw that line and Week 7's database swap is a change to one file.
Waiter and cook
The waiter takes the order, checks it makes sense, and carries the plate back. The cook knows how to make the dish and nothing about tables, tone of voice, or how to apologise. Neither can do the other's job well, and a kitchen where the cook keeps running out to the dining room is a kitchen where nothing gets cooked twice the same way.
Create src/equipment-store.ts:
export type Status = "operational" | "maintenance" | "down";
export const STATUSES: Status[] = ["operational", "maintenance", "down"];
export type Equipment = { id: string; name: string; serial: string; status: Status };
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" },
{ id: "eq-003", name: "Bench PSU", serial: "PSU-2020-441", status: "maintenance" }
];
let nextId = 4;
export function list(filter: { status?: Status }): Equipment[] {
if (!filter.status) return [...equipment];
return equipment.filter((item) => item.status === filter.status);
}
export function findById(id: string): Equipment | undefined {
return equipment.find((item) => item.id === id);
}
export function findBySerial(serial: string): Equipment | undefined {
return equipment.find((item) => item.serial === serial);
}
export function create(input: { name: string; serial: string; status: Status }): Equipment {
const created: Equipment = {
id: `eq-${String(nextId++).padStart(3, "0")}`,
name: input.name.trim(),
serial: input.serial,
status: input.status
};
equipment.push(created);
return created;
}
export function update(id: string, changes: Partial<Omit<Equipment, "id">>): Equipment | undefined {
const existing = findById(id);
if (!existing) return undefined;
Object.assign(existing, changes);
return existing;
}
export function remove(id: string): boolean {
const index = equipment.findIndex((item) => item.id === id);
if (index === -1) return false;
equipment.splice(index, 1);
return true;
}
equipment is not exported: the array is private to the module, and every change goes through a
named function. Omit<Equipment, "id"> (Day 33's generics) makes it a type error for update to
touch the id.
Notice how the functions report failure: findById returns undefined, remove returns false.
No status codes. Deciding that "not found" means 404 is the route's job, because 404 is an HTTP
idea.
Happy and failure paths
A happy path is the run where everything is valid. A failure path is any run where it is
not. Beginners test the first and ship the second untested, which is why so many APIs return 500
for a typo.
Write the matrix, then run every line of it. For this API:
| Request | Expect |
|---|---|
GET /health |
200 {"status":"ok"} |
GET /equipment |
200, { data, meta } |
GET /equipment?status=down |
200, only down records |
GET /equipment?status=nope |
400 |
GET /equipment?page=0 |
400 |
GET /equipment/eq-001 |
200, one record |
GET /equipment/eq-999 |
404 |
POST /equipment valid body |
201 + Location header |
POST /equipment empty name |
400 with details |
POST /equipment duplicate serial |
409 |
POST /equipment malformed JSON |
400 |
POST /equipment no Content-Type |
400 |
PATCH /equipment/eq-001 valid status |
200, updated record |
PATCH /equipment/eq-001 bad status |
400 |
PATCH /equipment/eq-001 changing id |
400 |
DELETE /equipment/eq-003 |
204, empty body |
DELETE /equipment/eq-003 again |
404 |
GET /nope |
404 |
Eighteen requests, twelve of them failures. That ratio is normal and it is the point.
Documenting the API
Two artefacts, both required today.
The endpoint table lives in README.md and lists, for every route: method, path, query
parameters, request body shape, response shape, and every status code it can produce. The status
codes are the part people forget and the part callers most need.
The request collection is the table made executable. A requests.http file is understood by
the REST Client extension in VS Code and by JetBrains IDEs — requests separated by ###, each a
method and URL, then headers, then a blank line, then the body:
### List all equipment
GET http://localhost:3000/equipment
### Filter by status
GET http://localhost:3000/equipment?status=down
### Create equipment
POST http://localhost:3000/equipment
Content-Type: application/json
{ "name": "Lathe 2", "serial": "LTH-2022-330", "status": "operational" }
### Reject an invalid status
PATCH http://localhost:3000/equipment/eq-001
Content-Type: application/json
{ "status": "melted" }
If you have no such extension, a shell script is just as good and runs anywhere:
#!/usr/bin/env bash
BASE=http://localhost:3000
curl -s -o /dev/null -w "GET /health -> %{http_code}\n" $BASE/health
curl -s -o /dev/null -w "GET /equipment?status=nope -> %{http_code}\n" "$BASE/equipment?status=nope"
curl -s -o /dev/null -w "GET /equipment/eq-999 -> %{http_code}\n" $BASE/equipment/eq-999
-w "%{http_code}" prints just the status, which turns the matrix into a checklist you can re-run
after every change.
Walkthrough: extract the store, prove nothing changed
Do this as a refactor — behaviour identical, structure better.
Record the current behaviour first. Run three requests and save the output; this is your before-picture.
Create
src/equipment-store.tsas above.In
src/server.ts, delete the array, theSTATUSESconstant, theStatusandEquipmenttypes and the id counter, and import instead. Under"module": "NodeNext"the import path uses a.jsextension even though the file is.ts— that is the ES-module rule, andtsxresolves it to the TypeScript file:import * as store from "./equipment-store.js"; import { STATUSES, type Status } from "./equipment-store.js";Replace each piece of data handling with a store call:
app.get("/equipment/:id", (req, res) => { const match = store.findById(req.params.id); if (!match) { res.status(404).json({ error: `No equipment with id "${req.params.id}"` }); return; } res.json(match); }); app.delete("/equipment/:id", (req, res) => { if (!store.remove(req.params.id)) { res.status(404).json({ error: `No equipment with id "${req.params.id}"` }); return; } res.status(204).end(); });The duplicate-serial check becomes
store.findBySerial(input.serial);POSTcallsstore.create(...);PATCHcallsstore.update(...);GET /equipmentcallsstore.list({ status })and then slices for pagination.Run
npm run typecheck, thennpm run dev, then the same three requests. Byte-for-byte identical output means the refactor is correct.
curl -s localhost:3000/equipment
{"data":[{"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"},{"id":"eq-003","name":"Bench PSU","serial":"PSU-2020-441","status":"maintenance"}],"meta":{"page":1,"limit":20,"total":3,"totalPages":1}}
Checkpoint
Open src/equipment-store.ts and confirm the words req, res, and every three-digit status
code are absent. If any appear, HTTP has leaked into your logic layer.
Your turn
Deliverable: a working API, a request collection, and an endpoint table.
- Create
src/equipment-store.tsand move all data handling into it.npm run typecheckmust pass. - Confirm no route handler touches an array directly, and the store mentions no status codes.
- Write the full failure matrix above into
README.md, adding any case specific to your version. - Build your request collection —
requests.httpor a curl script — with one entry per matrix row. - Run every entry against a freshly started server and record the actual status code beside each expectation. Any mismatch is today's bug list; fix them.
- Complete the endpoint table in
README.md: method, path, query parameters, request body, response body, and all status codes. - Add a short "Limitations" section stating that data is in memory and resets on restart, and that persistence arrives in Week 7.
- Commit the whole project with a message describing the refactor and the verification you ran.
Reviewer mode — after your matrix passes
"Perform a skeptical API review focused on validation, errors, duplicated logic, and missing negative tests." Give it your source and your matrix. A review must produce specific, actionable findings with evidence — a named route, a request that misbehaves, a case your matrix omits — not praise and not a rewrite. Reproduce each finding with curl before you accept it; anything you cannot reproduce is not a defect.
Common pitfalls
- A "store" that still speaks HTTP. If a store function returns
{ status: 404 }, the boundary is in the wrong place. Return data orundefined/false, and let the route choose the code. - Exporting the array itself. Any importer can then mutate it, and the store's rules — id generation, trimming — get bypassed. Keep it private, export functions.
- Testing only the happy path. The failure rows are the majority of the matrix and where real callers spend their time.
- Refactoring and adding features in one go. Then a broken response could be either. Move the code first, prove the output is identical, and only then change behaviour.
- Forgetting the
.jsextension in the import. Under"module": "NodeNext"an extensionless relative import fails. Write./equipment-store.jseven though the file is.ts.
Verify it yourself
Open today's reference, Express's Getting started pages, and read its guidance on structuring an application.
- Does Express's own documentation or examples separate route handlers from data access? Note what it does and whether it matches today's split.
- Find how Express documents
express.Router(). Write one sentence on how it would let you move the/equipmentroutes into their own file, and whether that is a different concern from the store. - This lesson claimed a
204response must have an empty body. Check that against MDN's page for204 No Contentand record what it says.
Put the answers in README.md. Next week the array in equipment-store.ts becomes a real database
— and because the routes never knew where the data was, that is one file's worth of change.
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
Test all endpoints with a REST client or curl. Move business logic out of route handlers.
- 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
A working API, request collection, and endpoint table.
Working with AI today
Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.
Perform a skeptical API review focused on validation, errors, duplicated logic, and missing negative tests.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.