Your First Server: HTTP, Ports, and Node.js
What does it mean to serve?
Chapter 9 — Your First Server: HTTP, Ports, and Node.js
“Be conservative in what you send, be liberal in what you accept.” — Jon Postel, RFC 760 (Postel’s Law)
“But whoever would be great among you must be your servant, and whoever would be first among you must be slave of all.” — Mark 10:43–44
Why This Matters
For eight weeks you built things that ran once and stopped. You typed python pipeline.py, the program woke up, did its work, printed its result, and died. Every program you have written in this book — and most of what you wrote in Coding 1 and 2 — has that shape: born, runs, exits.
A server is different. A server does not exit. It wakes up, opens a door, and then waits — for minutes, days, years — answering whoever knocks. The program’s whole life is spent waiting and responding. That is the single largest mental shift in this book, and it happens this week.
You have used servers your entire life without ever seeing one from the inside. Every web page, every app, every “log in” button has, on the other end, a program exactly like the ones you are about to write: a process sitting on a machine somewhere, parked at a numbered door, answering requests. This week you stop being only the person who sends the request and become the person who answers it. That is the entire content of Phase 2 in one sentence — you are crossing to the other side of the wire.
We will do it the hard way first, on purpose. No framework. Next week you will meet FastAPI, which hides almost everything we are about to learn behind two decorators. If you skip straight to the framework, you will be able to use a server without ever understanding what one is — and the day it breaks, you will have nothing. So this week we build the smallest real server by hand, with Node’s built-in http module and nothing else, so that when the framework hides the machinery next week, you will know exactly what it hid. That is the right-tool thesis turned inward: you cannot judge what a tool saves you until you have paid the price it saves you from.
This is also where the agent comes back on. Phase 1 was closed-AI on purpose — you cannot reason about the cost of a structure you have never built. Now, from Week 9 forward, agentic AI is part of the work, and it is agentic: it edits files, runs curl, reads errors, and iterates. But the project this week is shaped so the agent cannot finish it alone. The endpoint design — what resources exist, what each method means, which status code is correct for each case — is a series of judgment calls, and those are yours. The agent can type the route handler once you have decided the route exists. Deciding which routes should exist is architecture. That is the line you will hold for the next eight weeks.
The Christian question for the week is the plainest one in the book: what does it mean to serve? The word “server” is not an accident. A server is the thing that waits to be of use. It does not initiate; it responds. It does not decide what it wants; it answers what it is asked, as well as it can, and then waits to be asked again. There is a whole theology of vocation in that posture, and we will come back to it. For now, hold the image: the greatest among you will be the one who waits at the door.
9.1 — What a Server Actually Is
Strip away every buzzword and a server is this:
A server is a program that waits at a port and answers requests.
That is the whole definition. Let’s take it apart.
There are two roles in every networked interaction. The client is the program that initiates — it sends a request and waits for an answer. The server is the program that waits — it receives requests and sends back responses. Your browser is a client. The program behind google.com is a server. curl, which you will use all week, is a client. The Node program you are about to write is a server.
The conversation between them is the request/response cycle, and it is strictly turn-based:
- The client opens a connection to the server’s address and port.
- The client sends a request (“GET me the list of prayer requests”).
- The server reads the request, does whatever work it needs to, and sends back a response (“200 OK, here is the JSON”).
- The connection’s job is done for that exchange.
The client always speaks first. The server never reaches out unprompted — it cannot, because it does not know who its clients are until they knock. This is the inverse of the programs you have written so far, which decided for themselves what to do next. A server’s next action is decided by whoever is calling it. It has given up control of its own schedule. (Hold that thought against the epigraph.)
Coach’s Note — “Client” and “server” are roles, not machines. One program can be both: a web server that answers your browser is, at the same moment, a client of the database it queries to build the answer. Your FastAPI service next week will be a server to the browser and a client to Postgres. Don’t attach the words to hardware; attach them to who-initiates-this-particular-conversation.
9.2 — Ports and Addresses
A single computer runs many programs that might all want to talk to the network at once — a web server, a database, a mail program, your editor’s update checker. When a request arrives at the machine, the operating system has to know which program it is for. That is what a port is for.
An address identifies a machine. A port identifies a program on that machine. Together they form the destination of any network conversation.
| Term | What it is | Example |
|---|---|---|
| IP address | The numeric address of a machine on the network | 127.0.0.1, 93.184.216.34 |
localhost | A name that always means this same machine | resolves to 127.0.0.1 |
| Port | A number (0–65535) identifying a program on the machine | 3000, 80, 443, 5432 |
localhost:3000 | ”The program listening on port 3000, on this machine” | your dev server |
127.0.0.1 is special: it is the loopback address, which always means “the machine I am running on.” localhost is just a friendly name for it. When you point curl at http://localhost:3000, the request never leaves your computer — it goes out the network stack and immediately loops back to a program on the same machine. That is why you can develop a server with no internet connection at all.
Some port numbers are conventions you should recognize on sight:
| Port | Conventional use |
|---|---|
| 80 | HTTP (plain web traffic) |
| 443 | HTTPS (encrypted web traffic) |
| 5432 | PostgreSQL (you’ll meet it in Week 12) |
| 27017 | MongoDB (Week 13) |
| 3000 | No official meaning — a common dev-server default |
For development we use a high-numbered port like 3000 because ports below 1024 are “privileged” — the operating system requires administrator rights to use them, since they are reserved for well-known services. 3000 is fair game and asks no one’s permission.
“Listening on a port” means your program has asked the operating system: route every connection that arrives on this port to me. The OS agrees, and from then on, anything knocking on 3000 is handed to your program. Which leads to the rule that bites every beginner exactly once:
Two programs cannot listen on the same port at the same time.
The OS can route a port to one program. If your server is already running on 3000 and you start a second copy, the second one crashes with EADDRINUSE — “address already in use.” There is no ambiguity to resolve: the door has one doorkeeper. We will turn that exact error into a Common Bug below, because you will hit it.
Coach’s Note — When
EADDRINUSEstrikes, it is almost always a previous run of your own server that you forgot to stop. The port is a single physical resource, like a parking space — the new car can’t park until the old one leaves. Find the old process (lsof -i :3000on macOS/Linux) and stop it, or just pick a different port. Appendix A has the platform-specific commands.
9.3 — HTTP: The Language Servers Speak
Knowing the address gets your message to the right program. HTTP — HyperText Transfer Protocol — is the language the client and server speak once connected. It is, at heart, plain text with a strict shape.
An HTTP request has three parts, in order:
POST /requests HTTP/1.1 <- request line: METHOD PATH VERSION
Host: localhost:3000 <- headers (key: value), one per line
Content-Type: application/json
Content-Length: 38
{"title":"Pray for Maya's exam"} <- body (optional; here, JSON)
- The request line: the method, the path, and the HTTP version.
- The headers: key/value metadata about the request — what kind of body is attached, how long it is, who is asking.
- A blank line, then the optional body — the actual payload, here a chunk of JSON.
The server’s response has the same three-part shape:
HTTP/1.1 201 Created <- status line: VERSION CODE REASON
Content-Type: application/json
Content-Length: 52
{"id":4,"title":"Pray for Maya's exam"} <- body
A status line (version, a numeric status code, a human-readable reason), headers, a blank line, and the body. That is the entire protocol’s shape. Everything else is detail layered on top.
Methods: the verb of the request
The method says what kind of action the client wants. There are several, but four carry almost all the weight, and they map cleanly onto the four things you can do to a stored resource — the “CRUD” you’ll formalize with databases later:
| Method | Means | CRUD | Has a body? | Should it change server state? |
|---|---|---|---|---|
GET | ”Give me this resource” | Read | No | No — read-only |
POST | ”Create a new resource here” | Create | Yes | Yes — adds something |
PUT | ”Replace this resource with what I send” | Update | Yes | Yes — replaces |
DELETE | ”Remove this resource” | Delete | No | Yes — removes |
The crucial discipline: GET must never change anything on the server. A GET is a question, not a command. If clicking a link (which fires a GET) deleted data, the web would be unusable — every search-engine crawler, every “preload this link” optimization, every accidental refresh would wreak havoc. This is a contract, the Coding 2 skill in a new key: the method is a promise about what the request will do.
Status codes: the verdict of the response
The status code is a three-digit number that tells the client, at a glance, what happened. The first digit is the category:
| Range | Category | Meaning |
|---|---|---|
2xx | Success | It worked |
4xx | Client error | You sent something wrong |
5xx | Server error | I broke |
The specific codes you will use this week, and the rule for choosing each:
| Code | Name | Use it when… |
|---|---|---|
200 | OK | A GET succeeded and you’re returning data |
201 | Created | A POST succeeded and a new resource now exists |
400 | Bad Request | The client’s input was malformed or invalid (bad JSON, missing field) |
404 | Not Found | The requested resource does not exist (no item with id 99) |
500 | Internal Server Error | Your code threw an exception it didn’t expect |
Choosing the right code is not decoration — it is how your API tells the client whose fault a problem is and what to do about it. A 404 says “stop asking for this id; it isn’t here.” A 400 says “fix your request and try again.” A 500 says “this is my bug, not yours.” Returning 200 with an error message in the body — a beginner reflex — is a lie the client’s tooling cannot read. The status code is the truth; the body is the detail.
Coach’s Note — The single most common API design crime is returning
200 OKfor everything and burying"error": "not found"in the body. Now the client has to parse your prose to discover that its request failed — and no automated tool can. The status code exists so a machine can tell success from failure without reading the body. Honor it. This is Postel’s Law in practice: be precise and conservative in what you send.
Content types
The Content-Type header tells the receiver how to interpret the body’s bytes. The same bytes mean different things depending on this header. The one that matters all week is:
Content-Type: application/json
It says “the body is JSON.” Other common ones are text/html (a web page), text/plain (raw text), and application/x-www-form-urlencoded (HTML form data). When your server sends JSON, it must set this header, or clients that auto-parse responses will guess wrong. Sending JSON bytes with no Content-Type is like handing someone a sealed envelope with no idea what’s inside.
9.4 — Just-Enough Node.js
The servers in this chapter are written in JavaScript, run by Node.js. You have never written JavaScript, but you have written C++, Java, and Python, and JavaScript is closer to those than any of them is to the others. Here is the entire on-ramp you need for this week.
Node is JavaScript that runs outside the browser. JavaScript was born in 1995 to make web pages interactive inside a browser. Node took that same language and its engine and let it run as an ordinary program on a server — reading files, opening ports, answering requests. So “Node.js” is not a language; it is a runtime — the thing that runs JavaScript on a server, the way the JVM runs Java.
The syntax you need, mapped to what you already know:
// Variables: 'const' for things that won't be reassigned, 'let' for things that will.
const port = 3000; // like 'final int port = 3000;' in Java
let count = 0; // reassignable
count = count + 1;
// Functions, two ways. The arrow form is the one you'll see most:
function add(a, b) { return a + b; }
const addArrow = (a, b) => a + b; // same thing, "arrow function"
// Objects are key/value maps written with braces — exactly like JSON:
const request = { title: "Pray for Maya", urgent: true };
console.log(request.title); // dot access, like a field
// Arrays:
const items = [10, 20, 30];
items.push(40); // append
console.log(items.length); // 4
// String building uses backtick templates with ${...}:
const name = "Maya";
console.log(`Hello, ${name}.`); // -> Hello, Maya.
// printing:
console.log("anything"); // like System.out.println / print()
You import built-in modules with require:
const http = require("http"); // pull in the built-in HTTP module
That is genuinely most of it. JavaScript has sharp edges (== vs ===, which we will warn about), but for writing a small server you now know enough syntax to read every example in this chapter.
The one thing that is genuinely different: asynchrony
Here is where Node diverges from everything in Phase 1, and it is the direct payoff of the event-loop teaser from §8.7.
Node is single-threaded. One thread. There is no thread pool, no Lock, no GIL to fight, because there is only ever one thread of execution. And yet a Node server routinely handles thousands of simultaneous connections without breaking a sweat. You met the trick in Week 8: the event loop with non-blocking I/O.
Recall the single chef from Chapter 8. A blocking program is a chef who starts the pot of water boiling and then stands and stares at it until it boils, doing nothing else. Node’s chef starts the pot, and while it heats, turns to chop vegetables, start the oven, plate a finished dish — and returns to the pot only when it signals that it’s ready. One pair of hands, many dishes in progress, zero idle time.
Mechanically: when Node hits an I/O operation — a network read, a database query, a file read — it does not wait. It registers a callback (“when this finishes, run this function”) and immediately moves on to the next ready piece of work. When the I/O completes, the result is placed on a queue, and the single thread picks up the callback and runs it. The thread is never parked waiting; it is always either running a short burst of code or kicking off an I/O and moving on.
This is why the request handler you are about to write is a function Node calls for you — a callback. You don’t write a loop that says “wait for the next request.” You hand Node a function and say “call this whenever a request arrives,” and Node’s event loop does the waiting. Every example below has that shape.
Coach’s Note — The event loop’s strength is exactly its weakness, and you must hold both. One thread means no locks and no race conditions — beautiful, after the GIL battles of Week 8. But that same one thread means a single long synchronous computation freezes every connection at once, because there is no other thread to pick up the slack. Node is superb for many-connections, I/O-bound work and a poor fit for heavy CPU crunching — the mirror image of where Python’s
multiprocessingshines. The Hard tier of this week’s project makes you feel this. Don’t skip it.
9.5 — The Smallest Server That Serves
Here is the smallest Node server that actually serves. Eleven lines. Read it, then we’ll walk it.
// hello-server.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 it:
node hello-server.js
Your terminal prints Listening on http://localhost:3000 and then does not return to the prompt. That is the shift from §9.1 made real — the program is not done; it is waiting. Leave it running and, in a second terminal:
curl http://localhost:3000
You get back Hello from your first server. Press Ctrl-C in the first terminal to stop the server.
Now the walk-through:
require("http")pulls in Node’s built-in HTTP module. No install, no framework — it ships with Node.http.createServer(...)builds a server. We hand it a function — the request handler. This is the callback from §9.4: Node will call this function once per incoming request, handing us areq(the request) and ares(the response we get to fill in).- Inside, we set the status code (
200), set a header (Content-Type), and callres.end(...)with the body.res.endsends the response and closes the exchange. Every request must end in exactly oneres.end— forget it and the client hangs forever, waiting for a response that never finishes. server.listen(3000, callback)is the line that opens the door. It claims port3000and runs its callback once the server is up. This is “listening on a port” from §9.2, in code.
That is a complete, correct HTTP server. It ignores the method, ignores the path, and answers everything identically — but it serves. It is the MVP of servers: the smallest thing that genuinely does the job. We build outward from here.
9.6 — Routing: Method + Path
A real API does different things for different requests. GET /requests lists prayer requests; POST /requests creates one; GET /requests/3 fetches request number 3. The combination of method and path decides what happens. Choosing what that map is — which (method, path) pairs your server answers, and what each one does — is the design work that is yours, not the agent’s.
req.method gives you the method as a string ("GET", "POST"). req.url gives you the path ("/litman-books/requests", "/litman-books/requests/3"). Routing is just inspecting those two and branching:
const http = require("http");
const server = http.createServer((req, res) => {
res.setHeader("Content-Type", "application/json");
if (req.method === "GET" && req.url === "/litman-books/requests") {
res.statusCode = 200;
res.end(JSON.stringify({ message: "here is the list" }));
} else if (req.method === "POST" && req.url === "/litman-books/requests") {
res.statusCode = 201;
res.end(JSON.stringify({ message: "created" }));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: "not found" }));
}
});
server.listen(3000, () => console.log("Listening on :3000"));
Two pieces of JavaScript discipline live in there:
===, not==. JavaScript’s==does type coercion that will surprise you (0 == ""istrue). Always use===(strict equality, no coercion). This is the same vigilance Coding 1 drilled for==vs.equals()in Java — different language, same trap, same fix: use the strict comparison every time.JSON.stringify(obj)turns a JavaScript object into a JSON string — the bytes you send over the wire. Its inverse,JSON.parse(str), turns a JSON string back into an object. You send strings; you receive strings; these two functions cross the boundary.
Notice the fall-through else: any request that matches no route gets a 404. That is the right code — the client asked for something that isn’t there. An API with no catch-all 404 leaves unmatched requests hanging, which is worse than any error.
9.7 — Reading a Request Body
GET and DELETE carry no body. POST and PUT do — that’s where the new resource’s data lives. But here is the asynchronous reality from §9.4 made concrete: the body does not arrive all at once. It streams in over the network as a series of chunks, and Node hands you each chunk via a callback. You must collect the chunks, and only when the body is complete can you parse it.
The pattern is the same every time:
function readJsonBody(req, callback) {
let body = "";
req.on("data", (chunk) => { body += chunk; }); // a chunk arrived; accumulate
req.on("end", () => { // no more chunks; body is whole
try {
const parsed = JSON.parse(body);
callback(null, parsed); // success: (no error, the object)
} catch (err) {
callback(err, null); // bad JSON: report the error
}
});
}
req.on("data", fn)registers a callback that fires for each chunk. We append each chunk to a growing string.req.on("end", fn)fires once, when the body is fully received. Now — and only now — is it safe to parse.- We wrap
JSON.parseintry/catchbecause a client can send malformed JSON, andJSON.parsethrows on bad input. If we don’t catch it, the exception escapes our handler and — because there’s only one thread — it can take the whole server down. Catching it lets us answer400 Bad Requestand keep serving everyone else. This is the single most important defensive habit in the chapter.
The callback(error, result) shape — error first, result second — is the classic Node convention. The caller checks: was there an error? If so, send 400. If not, use the parsed object.
Coach’s Note — “Surviving malformed JSON” is a Medium-tier project requirement, and it is not busywork. A server is a public door — anyone can knock, and some will knock wrong, by accident or on purpose. A server that crashes on the first bad byte is not a server; it is a tantrum. Postel’s Law again: be liberal in what you accept — accept the bad request gracefully, answer
400, stay up. The skill of Phase 2 is building things that stay up.
9.8 — A Complete Minimal JSON API
Now we assemble the pieces into a real, runnable JSON API: an in-memory list of prayer requests, with GET (list and by-id) and POST (create), correct status codes throughout. This is the Normal tier of the project, in miniature. The full version with comments lives in code/prayer-api.js; read this one for the shape.
const http = require("http");
// In-memory "database" — a plain array. Dies when the server stops. That's fine for now.
let requests = [
{ id: 1, title: "Healing for Pastor John", answered: false },
{ id: 2, title: "Safe travel for the mission team", answered: false },
];
let nextId = 3;
function send(res, status, payload) {
res.statusCode = status;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(payload));
}
const server = http.createServer((req, res) => {
const { method, url } = req;
// GET /requests -> the whole list
if (method === "GET" && url === "/litman-books/requests") {
return send(res, 200, requests);
}
// GET /requests/:id -> one item, or 404
if (method === "GET" && url.startsWith("/litman-books/requests/")) {
const id = Number(url.split("/litman-books/")[2]);
const found = requests.find((r) => r.id === id);
if (!found) return send(res, 404, { error: `no request with id ${id}` });
return send(res, 200, found);
}
// POST /requests -> create one
if (method === "POST" && url === "/litman-books/requests") {
let body = "";
req.on("data", (chunk) => { body += chunk; });
req.on("end", () => {
let data;
try {
data = JSON.parse(body);
} catch (err) {
return send(res, 400, { error: "body must be valid JSON" });
}
if (typeof data.title !== "string" || data.title.trim() === "") {
return send(res, 400, { error: "title is required and must be a non-empty string" });
}
const created = { id: nextId++, title: data.title.trim(), answered: false };
requests.push(created);
return send(res, 201, created); // 201 Created, with the new resource
});
return; // stop here; the response happens inside the 'end' callback
}
// Nothing matched.
send(res, 404, { error: "not found" });
});
server.listen(3000, () => console.log("Prayer API on http://localhost:3000"));
Trace the design decisions, because these are the architecture — the part the agent can’t make for you:
GET /requestsreturns the array with200. A read, no body needed, success.GET /requests/:idparses the id out of the path, looks it up, and returns200with the item — or404if no such item exists. Choosing404here (not200withnull, not400) is the correct-status-code judgment from §9.3.POST /requestsreads the body, rejects malformed JSON with400, rejects a missing or empty title with400(validation — input at the door, the Coding 2 habit), and on success creates the resource and returns201 Createdwith the thing it created. Not200—201, because something new now exists.- The
return send(...)pattern guarantees exactly one response per request. The barereturn;after registering thePOSTcallbacks is load-bearing: it stops the handler from falling through to the catch-all404while the body is still streaming in.
Notice the helper: send(res, status, payload) sets the status, the JSON content-type, and the body in one call, so every response is consistent. When you find yourself writing the same three lines for every branch, extract the helper. That is the Coding 1 decomposition reflex, alive in a new language.
9.9 — Testing With curl
You do not need a browser to test an API. curl is a command-line HTTP client — it speaks the protocol from §9.3 directly — and it is how you will test every server in this book. With your prayer API running, open a second terminal:
# GET the list (200)
curl http://localhost:3000/requests
# GET one item (200)
curl http://localhost:3000/requests/1
# GET a missing item (404)
curl -i http://localhost:3000/requests/99
# POST a new request (201)
curl -X POST http://localhost:3000/requests \
-H "Content-Type: application/json" \
-d '{"title":"Pray for Maya'\''s exam"}'
# POST with malformed JSON (400, server stays up)
curl -X POST http://localhost:3000/requests \
-H "Content-Type: application/json" \
-d '{title: broken}'
The flags you need:
| Flag | Does |
|---|---|
| (none) | GET by default |
-X POST | Use the POST method (or PUT, DELETE) |
-H "..." | Add a header — set Content-Type: application/json when sending a body |
-d '...' | Send this string as the request body |
-i | Include the response headers and status line in the output |
Get in the habit of using -i constantly. It is the only way to see the status code, and the status code is half the contract. A response that looks right in the body but returns the wrong status is a bug — and -i is how you catch it. The full cheat sheet, including PUT and DELETE, is in code/curl-cheatsheet.sh.
Coach’s Note — Testing a server by hand with
curlis the Phase 2 equivalent of running your JUnit tests in Phase 1 — except the server has to be up in another terminal while you do it. Two terminals, always: one running the server, one poking it. Ifcurlhangs forever with no response, the bug is almost always a code path that forgot to callres.end. The client is doing exactly what the protocol says: waiting for a response you never finished sending.
9.10 — Why No Framework First
You could write all of this in Express (or FastAPI, next week) in a third of the lines. So why are we doing it the hard way?
Because a framework is a pile of decisions someone else made, hidden behind a friendly surface. Express turns the body-reading dance of §9.7 into app.use(express.json()) and the routing of §9.6 into app.get("/litman-books/requests", handler). That is genuinely better once you know what it’s doing — and a genuine trap if you don’t, because the day it misbehaves, you will have no model of the machinery underneath to debug against.
This week you learned the machinery: a server is a callback Node invokes per request; a body streams in as chunks you must collect; a response is a status code plus headers plus a body you must end; the right status code is a judgment, not a default. Next week, FastAPI will hide every one of those things behind clean Python. And because you built it by hand first, you will look at FastAPI’s two-line endpoint and know, precisely, what it is doing for you — and therefore when to trust it and when to reach past it.
That is the right-tool thesis stated one more way: you cannot judge what a tool saves you until you have paid the price it saves you from. You just paid it. Next week you collect the savings — with open eyes.
9.11 — Common Bugs
Bug: The server starts but curl hangs forever and never gets a response.
Example: A route does its work but never calls res.end() (or returns before reaching it).
Fix: Every code path through the handler must end in exactly one res.end (or your send helper). Trace each branch and confirm it terminates the response. A hung client is almost always a missing end.
Bug: Error: listen EADDRINUSE: address already in use :::3000 on startup.
Example: A previous run of your server is still alive and still holds port 3000 (§9.2 — one doorkeeper per door).
Fix: Stop the old process (lsof -i :3000 then kill <pid> on macOS/Linux; see Appendix A) or start on a different port. This is not a code bug; it’s a resource conflict.
Bug: POST handler crashes the entire server when sent non-JSON.
Example: const data = JSON.parse(body); with no try/catch. A malformed body throws, the exception escapes the single thread, and the process dies — taking every connection with it.
Fix: Wrap every JSON.parse of client input in try/catch and answer 400 on failure (§9.7). Never trust the body.
Bug: Two if branches both run, or the catch-all 404 fires even on a matched route.
Example: Forgetting the return before send(...), so the handler falls through into the next branch after sending a response — then tries to send again (“Cannot set headers after they are sent”).
Fix: return after every send. One request, one response, then stop.
Bug: url.split("/litman-books/")[2] gives NaN after Number(...), so the lookup never matches.
Example: Requesting /requests/abc — Number("abc") is NaN, and NaN === anything is false, so .find returns nothing.
Fix: This actually behaves correctly (you’ll return 404, which is right for a non-numeric id), but be aware of it. If you want a distinct message, check Number.isNaN(id) and return 400 “id must be a number.”
Bug: Comparing with == and getting a surprising match.
Example: if (data.answered == false) matches when answered is 0, "", null, or undefined, because == coerces types.
Fix: Use === everywhere (§9.6). It never coerces. Same vigilance as == vs .equals() in Java.
9.12 — Reps
Open the exercises for the full set. This week’s reps build the server muscle from zero: a one-line responder, then routing, then body parsing, then a full small API, then curl-driven testing — exactly the skills the project will demand. AI/agents are ON for Phase 2, but the reps are hand-built unless a rep says otherwise; you cannot direct an agent to build a server you have never built yourself.
A preview:
- Rep 1 — Run the eleven-line server. Hit it with
curl. Watch it wait. - Rep 3 — Route by method and path: a different answer for
GET /pingandPOST /echo. - Rep 6 — Read and parse a JSON body, surviving malformed input with a
400. - Rep 11 — Build a complete two-method in-memory API and test every endpoint with
curl.
Do every one. Type every line.
9.13 — This Week’s Project
You’re ready for Project 9 — A JSON API in Node, in Project 9.
You will build a real JSON API with Node’s http module and no framework: GET (list and by-id) and POST with correct status codes (Normal); PUT, DELETE, body validation with 400s, a /health endpoint, and malformed-JSON survival (Medium); concurrent-request handling that proves the event loop keeps serving during a deliberately slow endpoint, plus the decision memo on when Node’s single-threaded event loop is the right tool and when it is the wrong one (Hard).
This is the first project of Phase 2, and the first that requires an agent-log.txt. The agent can write route handlers once you’ve decided the routes exist — but which resources exist, which methods they support, and which status code is correct for each case are your decisions, documented and defended. The agent builds the modules. You decide which modules exist and why.
9.14 — Coach’s Final Word for Week 9
You crossed a line this week. For eight weeks you were the one sending requests. Now you are the one answering them. You stood up a program that does not exit — that waits at a numbered door and serves whoever knocks — and you did it with no framework, so you know exactly what a server is before any framework gets to hide it from you.
The Christian frame is not decoration here, and it is worth sitting with. A server is the thing that waits to be of use. It does not set its own agenda; it answers what it is asked, as well as it can, and waits to be asked again. “Whoever would be great among you must be your servant.” The greatest program on the network is not the one that demands the most — it is the one that serves the most, reliably, without crashing on the bad request, returning the honest status code, staying up so the next person who knocks gets an answer too. There is a posture of vocation in that. A well-built server is a small, faithful servant. Build it to stay up.
If you find this hard: that’s the gap. Close it. A server is just a program that waits at a port and answers requests — you built one in eleven lines. Everything else is detail layered on that sentence.
Next week, FastAPI hides this machinery — and you’ll finally see what a framework is for, because you’ll know what it’s hiding.
See you on Monday.
Up next: Read the exercises and complete every rep — type every line, run every server, test with curl. Then open Project 9 and build your first API (and your first agent-log.txt). After that, Chapter 10 — the same API again, in FastAPI, so the choice between stacks becomes a decision you have felt from both sides. (Coming from the midterm? Chapter 8 — concurrency.)