Building an HTTP Server
Node’s node:http module lets you build a web server with no dependencies. Frameworks like Express are built on top of it, so understanding it pays off.
A Minimal Server
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from Node!");
});
server.listen(3000, () => {
console.log("http://localhost:3000");
});
Run it, then open http://localhost:3000 in a browser.
The Request Object
req describes what the client asked for:
createServer((req, res) => {
console.log(req.method); // "GET", "POST", ...
console.log(req.url); // "/about?x=1"
console.log(req.headers); // { host, user-agent, ... }
});
Parse the URL and query string:
const url = new URL(req.url, "http://localhost");
console.log(url.pathname); // "/about"
console.log(url.searchParams.get("x")); // "1"
The Response Object
res is how you answer:
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
writeHead(status, headers)sets the status and headers.end(body)finishes the response.
Routing by Hand
createServer((req, res) => {
const { pathname } = new URL(req.url, "http://localhost");
if (req.method === "GET" && pathname === "/") {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Home");
} else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not found");
}
}).listen(3000);
This works, but it gets messy fast — that is why frameworks exist.
Reading a Request Body
Body data arrives as a stream:
let body = "";
for await (const chunk of req) {
body += chunk;
}
const data = JSON.parse(body || "{}");
Best Practices
- Always set
Content-Type— Browsers and clients depend on it. - Send the right status code — 200, 201, 404, 500.
- Use a framework for real apps — Express or Fastify handle routing, parsing, and errors.
- Close responses — Every request needs an
end()or it hangs.
Common Mistakes
- Forgetting
res.end()— The request hangs forever. - Parsing
req.urlas a full URL — It is a path; givenew URLa base. - Blocking the event loop — CPU-heavy work stalls every request.
- Ignoring the request method —
GET /xandPOST /xare different.