0 / 91
Week 6 · Day 42 of 91

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.

  1. Record the current behaviour first. Run three requests and save the output; this is your before-picture.

  2. Create src/equipment-store.ts as above.

  3. In src/server.ts, delete the array, the STATUSES constant, the Status and Equipment types and the id counter, and import instead. Under "module": "NodeNext" the import path uses a .js extension even though the file is .ts — that is the ES-module rule, and tsx resolves it to the TypeScript file:

    import * as store from "./equipment-store.js";
    import { STATUSES, type Status } from "./equipment-store.js";
    
  4. 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); POST calls store.create(...); PATCH calls store.update(...); GET /equipment calls store.list({ status }) and then slices for pagination.

  5. Run npm run typecheck, then npm 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.

  1. Create src/equipment-store.ts and move all data handling into it. npm run typecheck must pass.
  2. Confirm no route handler touches an array directly, and the store mentions no status codes.
  3. Write the full failure matrix above into README.md, adding any case specific to your version.
  4. Build your request collection — requests.http or a curl script — with one entry per matrix row.
  5. 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.
  6. Complete the endpoint table in README.md: method, path, query parameters, request body, response body, and all status codes.
  7. Add a short "Limitations" section stating that data is in memory and resets on restart, and that persistence arrives in Week 7.
  8. 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 or undefined/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 .js extension in the import. Under "module": "NodeNext" an extensionless relative import fails. Write ./equipment-store.js even 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.

  1. Does Express's own documentation or examples separate route handlers from data access? Note what it does and whether it matches today's split.
  2. Find how Express documents express.Router(). Write one sentence on how it would let you move the /equipment routes into their own file, and whether that is a different concern from the store.
  3. This lesson claimed a 204 response must have an empty body. Check that against MDN's page for 204 No Content and 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

  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

    Test all endpoints with a REST client or curl. Move business logic out of route handlers.

  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

A working API, request collection, and endpoint table.

Working with AI today

AI as skeptical reviewer

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

Q1 Why separate business logic from route handlers?
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.