Chapter 9 — Reps
Conditioning, not grading. Node.js + curl reps this week — your first servers.
Ground rules:
- Type every line yourself. No copy-paste. The point is the muscle, and you don’t build muscle watching someone else lift.
- Run every server. A server you didn’t start is a server you didn’t test. Two terminals, always: one runs the server, one pokes it with
curl. - AI/agents are ON now — Phase 2. But this week’s reps are hand-built. You cannot direct an agent to build a server you have never built yourself; that judgment comes only from having done it. Save the agent for the project, where the log will prove you stayed in charge.
- Use
curl -iconstantly. The status code is half the contract.-iis how you see it.
You need Node installed and on your PATH (node --version should print a version). If it doesn’t, do that first — Appendix A walks the install for every platform. Everything below assumes you can run node file.js from a terminal.
Reps 1–3: A Server That Waits
Rep 1 — Hello, Server
Type this into hello.js:
const http = require("http");
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain");
res.end("Hello from your first server.\n");
});
server.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});
Run node hello.js. Notice the terminal does not return to the prompt — the program is waiting, not finished. That is the whole shift of the week. In a second terminal:
curl http://localhost:3000
Confirm you get the greeting back. Then press Ctrl-C in the first terminal to stop the server. Run curl again — confirm it now fails (“connection refused”). The door is closed because the doorkeeper went home.
Rep 2 — See the Whole Response
Start hello.js again. This time hit it with the -i flag:
curl -i http://localhost:3000
Read the output line by line. You should see the status line (HTTP/1.1 200 OK), the headers (including the Content-Type: text/plain you set), a blank line, then the body. That three-part shape is the HTTP response from §9.3 — you are now looking at the protocol on the wire. Change res.statusCode to 404, restart, and run curl -i again. Watch the status line change to 404 Not Found. The reason phrase (“Not Found”) comes for free from the number.
Rep 3 — Route by Method and Path
Build router.js that answers two routes differently and 404s everything else:
GET /ping→ status200, bodypongPOST /echo→ status200, bodyecho received- anything else → status
404, bodynot found
Use req.method and req.url, if/else if/else, and === (never ==). Test all three with curl:
curl -i http://localhost:3000/ping
curl -i -X POST http://localhost:3000/echo
curl -i http://localhost:3000/nope
Confirm the third returns 404. A router with no catch-all leaves unmatched requests hanging — never ship one.
Reps 4–6: JSON and Request Bodies
Rep 4 — Send JSON, Set the Header
Modify router.js so GET /ping returns JSON instead of plain text: body {"message":"pong"}, with Content-Type: application/json. Build the body with JSON.stringify({ message: "pong" }) — never hand-type JSON strings. Confirm with curl -i that the Content-Type header reads application/json. Then deliberately remove the setHeader line and re-run: the body looks the same, but the header is wrong, and a real client would mis-parse it. Restore the header. The lesson: JSON bytes with no Content-Type is a sealed envelope with no label.
Rep 5 — A send Helper
You are about to write res.statusCode = ...; res.setHeader(...); res.end(JSON.stringify(...)) over and over. Extract it now:
function send(res, status, payload) {
res.statusCode = status;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(payload));
}
Rewrite Rep 3’s router to use send in every branch. Confirm all three routes still behave identically under curl. This is the Coding 1 decomposition reflex in a new language: when three lines repeat in every branch, they want to be one function.
Rep 6 — Read a JSON Body (and Survive Garbage)
Build echo-body.js with one route: POST /echo. It must read the request body (which streams in as chunks), parse it as JSON, and return it back with 200. If the body is not valid JSON, it must return 400 with {"error":"body must be valid JSON"} — and the server must stay up.
req.on("data", (chunk) => { body += chunk; });
req.on("end", () => {
try {
const data = JSON.parse(body);
send(res, 200, { youSent: data });
} catch (err) {
send(res, 400, { error: "body must be valid JSON" });
}
});
Test both paths:
curl -i -X POST http://localhost:3000/echo -H "Content-Type: application/json" -d '{"hi":"there"}'
curl -i -X POST http://localhost:3000/echo -H "Content-Type: application/json" -d '{broken}'
The second must return 400 and leave the server running — run the first one again afterward to prove it. Now delete the try/catch and re-run the malformed request: the server crashes and dies. Restore the try/catch. That is why we never trust the body.
Reps 7–9: A Resource With Status Codes
Rep 7 — GET a List and GET by Id
Build items.js with an in-memory array of three objects ({ id, name }) and two routes:
GET /items→200, the whole array.GET /items/:id→200with the one item, or404if no item has that id.
Parse the id with Number(req.url.split("/litman-books/")[2]) and find it with array.find(x => x.id === id). Test all three cases (list, a real id, a missing id) with curl -i, and confirm the missing-id case returns 404 — not 200 with null. Choosing 404 here is the correct-status-code judgment; make it on purpose.
Rep 8 — POST to Create, Return 201
Add POST /items to items.js. It must:
- Read and parse the JSON body (survive garbage with
400, per Rep 6). - Validate:
namemust be a non-empty string, or return400with a clear message. - Create the item with a fresh id, push it to the array, and return
201 Createdwith the new item.
Why 201 and not 200? Because something new now exists — that’s exactly what 201 means. Test it:
curl -i -X POST http://localhost:3000/items -H "Content-Type: application/json" -d '{"name":"Bible"}'
curl -i -X POST http://localhost:3000/items -H "Content-Type: application/json" -d '{"name":""}'
First returns 201 with the created item; second returns 400. Then GET /items and confirm your new item is in the list.
Rep 9 — The Status-Code Drill
No new code. Take your items.js and, for every route, write down on paper the status code it returns in each case, then prove each one with curl -i. Fill this table:
| Request | Expected status | Got it? |
|---|---|---|
GET /items | 200 | |
GET /items/1 (exists) | 200 | |
GET /items/99 (missing) | 404 | |
POST /items valid body | 201 | |
POST /items empty name | 400 | |
POST /items malformed JSON | 400 | |
GET /nonsense | 404 |
Every row must match. If one doesn’t, your status code is lying about what happened — fix it. This drill is the project’s Normal tier in miniature; do it cold and the project is half done.
Reps 10–11: PUT, DELETE, and Health
Rep 10 — PUT and DELETE
Add two routes to items.js:
PUT /items/:id→ replace the named fields of the item with that id. Return200with the updated item, or404if it doesn’t exist. Validate the body likePOSTdoes.DELETE /items/:id→ remove the item with that id. Return200(or204 No Content) on success,404if it didn’t exist.
DELETE carries no body — don’t read one. Test the full lifecycle with curl:
curl -i -X POST http://localhost:3000/items -H "Content-Type: application/json" -d '{"name":"Hymnal"}'
curl -i -X PUT http://localhost:3000/items/4 -H "Content-Type: application/json" -d '{"name":"Hymnal (2nd ed.)"}'
curl -i -X DELETE http://localhost:3000/items/4
curl -i http://localhost:3000/items/4 # now 404
You have now built all four CRUD verbs by hand. That is the whole Medium tier’s foundation.
Rep 11 — A /health Endpoint and the Full API
Add GET /health to items.js, returning 200 with {"status":"ok","items": <count>}. A health endpoint is how monitoring tools (and you) ask a running server “are you alive?” without disturbing real data — it’s the first thing operators reach for, and it costs you four lines.
Now run the complete items.js — GET list, GET by id, POST, PUT, DELETE, /health — and exercise every endpoint with curl -i, watching every status code. This is a complete, framework-free JSON API that you built from one require("http"). Sit with that. Next week FastAPI will do all of it in a fraction of the lines — and you’ll know exactly what it hid.
Done? One Last Thing.
From scratch, no looking — new file, blank editor. Build a notes.js server with an in-memory array of notes ({ id, text }) and exactly these routes:
GET /notes→200, the list.GET /notes/:id→200or404.POST /notes→ validatetextis a non-empty string;201on success,400on bad/missing body or malformed JSON (server must survive it).GET /health→200,{"status":"ok"}.- anything else →
404.
Use a send helper. Use ===. Wrap every JSON.parse of input in try/catch. End every path in exactly one response. Then test all of it with curl -i and confirm every status code by eye.
If you can do that cold — a correct, crash-proof, right-status-code JSON API from a blank file — you have the move, and Project 9’s Normal tier is yours.
Up next: Project 9 — Project 9: A JSON API in Node. Your first Phase 2 project, and your first agent-log.txt.