Without notes, state yesterday’s main idea and one unresolved question.
Build a server with node:http
HTTP, Node.js, and APIs
Objective
See the raw request lifecycle before using Express.
HTTP is a request/response protocol like a defined communication bus: method, address, headers, body, and status all have roles.
- createServer
- request and response objects
- listen and close
Why this matters
Yesterday your program read a file and stopped. Today it starts and keeps running, waiting for
HTTP requests and answering them — a server, the thing you have been sending requests to since
Day 1. You will write it with nothing but Node's built-in node:http module, no framework.
That is deliberate. Tomorrow you switch to Express, and every convenience Express offers will make sense only if you have first done the work yourself. Today you see the raw request lifecycle: a request object arrives, you inspect it, you write a response, and you end it.
createServer: one function, called on every request
node:http gives you createServer. You hand it a function; Node calls that function once for
every request that arrives, passing two objects: the request and the response.
import { createServer } from "node:http";
const server = createServer((req, res) => {
// this runs once per incoming request
});
That callback is the whole server. Everything else this week — routing, validation, JSON — is logic you put inside it or, from tomorrow, logic a framework arranges around it.
A server is an interrupt handler on the network bus
createServer registers a handler; listen enables the interrupt. From then on the process sits
idle until a request arrives on the port, at which point your handler runs with the incoming frame
in req and an outgoing frame buffer in res. Two consequences follow directly. Your handler
must finish quickly, because further requests queue behind slow work. And it must always write
and terminate a response — an interrupt handler that never clears the flag leaves the caller
waiting forever.
The request object
req is the incoming message, parsed into properties. The three you need today map exactly onto
what you read yesterday:
req.method— the string"GET","POST", and so on.req.url— the path and query string only, for example/equipment?status=down. Not the scheme or host; those went intoreq.headers.host.req.headers— an object of headers, with lowercased names:req.headers["content-type"].
Reading a request body is more work: it arrives as a stream of chunks that you must collect and
join. You do not need it today, because GET requests have no body. Tomorrow Express does the
collecting for you, and that is one of the main reasons to use it.
The response object
res is how you build the reply, and it must be built in protocol order — status line, then
headers, then body — because that is the order the bytes go out.
res.writeHead(statusCode, headersObject)writes the status line and headers in one call.res.setHeader(name, value)sets one header; only works before the head is written.res.write(chunk)appends to the body.res.end()finishes the response and sends it.res.end(chunk)writes a last chunk and finishes.
res.end() is not optional. Until you call it, the client is still waiting: the browser tab
spins, curl hangs. Forgetting it is the single most common mistake in raw Node servers, and nothing
in your terminal tells you it happened.
For JSON you owe the client two things: a body produced by JSON.stringify, and a
Content-Type: application/json header saying what it is. It is worth adding Content-Length too
— the byte count of the body — so the client knows exactly when the message is complete.
function sendJson(res, statusCode, payload) {
const body = JSON.stringify(payload);
res.writeHead(statusCode, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
});
res.end(body);
}
Buffer.byteLength counts bytes, not characters — an accented letter is one character but two
bytes, and HTTP counts bytes.
listen and close
A handler that is never attached to a port does nothing. server.listen(port, callback) starts
accepting connections on a host and port and runs the callback once it is ready.
On Day 5 you learned that a port is a numbered door on a machine and that only one program can hold a given port at a time. That rule is about to bite you: start a second copy of your server on port 3000 and Node stops with
Error: listen EADDRINUSE: address already in use :::3000
EADDRINUSE means a previous copy is still running. Stop it — Ctrl+C in the
terminal where it runs — or use a different port. (server.close() stops accepting new connections
from inside the program; you rarely call it by hand outside of tests.)
Because listen keeps the process alive, the terminal running your server is now busy. Open a
second terminal tab to run curl.
Your data lives in a variable, and that is on purpose
This week the equipment records sit in a plain array in memory. When the process stops, every change is gone. That is not a shortcut you will forget to fix — it is the point of doing servers before databases. Storing data durably needs a database, and that is Week 7. Until then, restart the server and you are back to the starting records.
Walkthrough: a server with two routes
In ~/fullstack-journey/, create raw-server/ with a package.json containing
{ "type": "module" }, then server.mjs:
import { createServer } from "node:http";
const equipment = [
{ id: "eq-001", name: "Feed Pump 3", status: "operational" },
{ id: "eq-002", name: "Cooling Fan A", status: "down" }
];
function sendJson(res, statusCode, payload) {
const body = JSON.stringify(payload);
res.writeHead(statusCode, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
});
res.end(body);
}
const server = createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
if (req.method === "GET" && req.url === "/health") {
sendJson(res, 200, { status: "ok" });
return;
}
if (req.method === "GET" && req.url === "/equipment") {
sendJson(res, 200, equipment);
return;
}
sendJson(res, 404, { error: "Not found" });
});
server.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});
Every branch checks both the method and the path, because GET /equipment and
POST /equipment are different requests. The last sendJson is the fallback: anything unmatched is
a 404, a client error, because the caller asked for something that is not there.
node server.mjs
Listening on http://localhost:3000
The prompt does not come back — the process is alive and waiting. In a second terminal:
curl -i http://localhost:3000/health
-i includes the response headers. You get:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15
Date: Mon, 03 Aug 2026 13:47:21 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"status":"ok"}
Everything from Day 36, now produced by code you wrote. Meanwhile the server terminal prints
GET /health — your console.log, the server-side view of the same exchange.
curl -s http://localhost:3000/equipment
curl -i http://localhost:3000/nope
[{"id":"eq-001","name":"Feed Pump 3","status":"operational"},{"id":"eq-002","name":"Cooling Fan A","status":"down"}]
HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 21
...
{"error":"Not found"}
The browser is an HTTP client too
Open http://localhost:3000/equipment in your browser. You see the same JSON — the browser sent
exactly the request curl did. Now open DevTools → Network and reload: the entry shows method
GET, status 200, and Content-Type: application/json, matching curl line for line.
Checkpoint
Say what each of createServer, listen, req.url, res.writeHead, and res.end does, and
predict what happens to curl if you delete the res.end(body) line. (It hangs.)
Your turn
Build the deliverable: a raw Node server with two routes and JSON responses.
- Create
raw-server/withpackage.json({ "type": "module" }) andserver.mjsas above. - Start it with
node server.mjsand confirm the "Listening" line. - In a second terminal,
curl -i http://localhost:3000/health. Confirm status200andContent-Type: application/json. curl -s http://localhost:3000/equipment. Confirm both records come back.- Open both URLs in the browser and check the Network tab entries against the curl output.
- Request an unknown path and confirm
404. curl -i -X POST http://localhost:3000/equipment. You get404, because your condition requiresGET. Write down in one line why405 Method Not Allowedwould be the more honest answer here.- Add a third route,
GET /equipment/count, returning{ "count": 2 }derived from the array length. Restart the server — Node does not reload on save — and verify with curl. - Deliberately comment out
res.end(body)insidesendJson, restart, and run curl. Watch it hang, press Ctrl+C, then restore the line. Feeling that hang once is what makes you remember it.
Pair mode — while you build step 8
"Guide me through a raw Node HTTP server. Explain each response header and why the response must end." Pair mode means the AI proposes and you decide. Before accepting anything it produces, inspect the change, run curl against it, and be able to explain the behaviour line by line. Commit nothing you cannot narrate.
Common pitfalls
- Forgetting
res.end(). The request hangs with no error anywhere. If curl sits there doing nothing, this is almost always why. EADDRINUSE. An older copy of the server still owns port 3000. Stop it with Ctrl+C in its terminal, or pick another port.- Editing and not restarting. Node runs the file it loaded at startup. Every change needs a stop and start until you use a watcher (tomorrow).
- Matching
req.urlwith===when there is a query string./equipment?status=downis not equal to/equipment, so the branch silently misses and you get a404. Express solves this tomorrow by separating path from query.
Verify it yourself
Open today's reference, the Node.js Introduction, and follow it to the HTTP server example.
- Does Node's own example set
Content-Typethe same way this lesson did? Note any difference. - Node's docs show
res.statusCode = 200andres.setHeader(...)as an alternative towriteHead. Try that form in your server and confirm the curl output is identical. - Find what the docs say
listendoes, and check it matches the claim above: that it starts accepting connections on a host and port.
Record the answers in the server file as comments. Tomorrow Express will hide most of this — you should be able to say exactly what it is hiding.
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
Create a server with GET /health and GET /equipment. Test with browser and curl.
- 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 raw Node server with two routes and JSON responses.
Working with AI today
Define one small task, review the plan, inspect the diff, run checks, and explain every changed section.
Guide me through a raw Node HTTP server. Explain each response header and why the response must end.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.