Chapter 09 · Reps

Your First Server: HTTP, Ports, and Node.js — Reps

← Back to Chapter 9

Chapter 9 — Reps

Conditioning, not grading. Node.js + curl reps this week — your first servers.

Ground rules:

  1. Type every line yourself. No copy-paste. The point is the muscle, and you don’t build muscle watching someone else lift.
  2. 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.
  3. 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.
  4. Use curl -i constantly. The status code is half the contract. -i is 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 → status 200, body pong
  • POST /echo → status 200, body echo received
  • anything else → status 404, body not 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 /items200, the whole array.
  • GET /items/:id200 with the one item, or 404 if 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:

  1. Read and parse the JSON body (survive garbage with 400, per Rep 6).
  2. Validate: name must be a non-empty string, or return 400 with a clear message.
  3. Create the item with a fresh id, push it to the array, and return 201 Created with 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:

RequestExpected statusGot it?
GET /items200
GET /items/1 (exists)200
GET /items/99 (missing)404
POST /items valid body201
POST /items empty name400
POST /items malformed JSON400
GET /nonsense404

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. Return 200 with the updated item, or 404 if it doesn’t exist. Validate the body like POST does.
  • DELETE /items/:id → remove the item with that id. Return 200 (or 204 No Content) on success, 404 if 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.jsGET 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 /notes200, the list.
  • GET /notes/:id200 or 404.
  • POST /notes → validate text is a non-empty string; 201 on success, 400 on bad/missing body or malformed JSON (server must survive it).
  • GET /health200, {"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.