Course outline · 0% complete

0/29 lessons0%

Course overview →

http.createServer, Line by Line

lesson 3-1 · ~10 min · 8/29

What .on means in Node

In lesson 2-3 you wrote process.stdin.on("data", callback), and .on generally means when this named event happens, run this callback.

.on(event, callback) registers a listener, so every time the event fires, the callback runs. Streams fire "data" and "end", and an HTTP server fires "request" for every incoming request.

The key difference from a plain callback is repetition. A callback passed to fs.readFile runs once, and a listener registered with .on runs every time the event happens, which is why one registration can serve thousands of requests.

It is the event-loop model from lesson 2-2 wearing different hats. Each fired event puts a callback in the queue, and the loop runs it when the stack is clear, whether the source was a timer, a socket, or a file.

A real server in nine lines

Node's built-in http module turns your request-to-response function into an actual network server:

const http = require("node:http");

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello from Node!");
});

server.listen(3000, () => {
  console.log("listening on http://localhost:3000");
});
  • createServer(callback) builds a server. The callback runs once per incoming request (there is the event loop again).
  • req describes the request: req.method, req.url, req.headers.
  • res is how you answer: res.writeHead(status, headers) then res.end(body).
  • listen(3000) claims port 3000, the routing number from lesson 1-2: from this moment the operating system delivers every connection addressed to localhost:3000 to this process. If another program already holds the port, listen fails with the famous EADDRINUSE error.

Save it as server.js, run node server.js on your own machine, and open http://localhost:3000 in a browser. Because these pages are static, server wiring is shown as code to read, while everything the server does is shown with its output.

requestGET /healthcreateServercallbackroute()your pure logicres.endreply
Keep the decision logic in a plain function. The http wiring stays thin, and the interesting part stays testable.

Separate the wiring from the logic

Professional Node code keeps the http wiring thin and pushes decisions into plain functions:

const server = http.createServer((req, res) => {
  const [status, body] = route(req.method, req.url);
  res.writeHead(status, { "Content-Type": "text/plain" });
  res.end(body);
});

Now route is just JavaScript. You can run it, test it, and reason about it with no server at all, which is exactly what you did in lesson 1-1 with handle. Build it below.

A route function you can run

route(method, path) maps a verb and a path to a status and a body, and the loop at the bottom exercises three cases.

function route(method, path) {
  if (method === "GET" && path === "/") return [200, "Welcome!"];
  if (method === "GET" && path === "/health") return [200, "ok"];
  if (method === "POST" && path === "/users") return [201, "user created"];
  return [404, "not found"];
}

const tests = [["GET", "/health"], ["POST", "/users"], ["DELETE", "/users"]];
for (const [m, p] of tests) {
  const [status, body] = route(m, p);
  console.log(m + " " + p + " -> " + status + " " + body);
}

Output

GET /health -> 200 ok
POST /users -> 201 user created
DELETE /users -> 404 not found

Each route is one line of the form if (method === ... && path === ...) return [status, body];, so both the verb and the path have to match. Matching on the path alone is the beginner shape, and it makes DELETE /users behave like GET /users.

201 is the status for created, used when a POST makes a new thing. Returning 200 there is not wrong enough to break a client and it throws away information, since 201 tells the caller a resource now exists.

DELETE /users matches no rule, so it falls through to the 404 return. Strictly, a path that exists for one verb and not another should answer 405 Method Not Allowed, and lesson 3-3 covers when that distinction is worth the extra code.

The array return with destructuring at the call site keeps the function honest. route decides and returns data, and nothing in it touches res, which is what makes this loop possible at all.

Because it is plain JavaScript, this function is directly testable with no server, no port, and no network. That is the payoff of separating wiring from logic, and it is the same shape as handle from lesson 1-1 grown one step.