"Frameworks hide the http module. Reading the http module makes you not need the framework — or at least understand what it's doing on your behalf."
A Server in 10 Lines
import { createServer } from 'node:http';
const server = createServer((req, res) => {
// req is an http.IncomingMessage (a Readable stream)
// res is an http.ServerResponse (a Writable stream)
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`hi, you asked for ${req.url}\n`);
});
server.listen(3000, () => {
console.log('http://localhost:3000');
});
That's a complete, production-shaped HTTP server. No Express, no Koa, no Fastify. Every framework you've ever used is a layer on top of this exact API. The handler runs for every request; req and res are streams; listen() binds to a port.
The Anatomy of a Request
req (IncomingMessage) carries:
req.method— 'GET', 'POST', etc.req.url— the path + query string, e.g./api/users?id=5.req.headers— an object of lowercased header names to values.- The body is a Readable stream — consume with
for await (const chunk of req).
The Anatomy of a Response
res (ServerResponse) lets you:
res.writeHead(statusCode, headersObject)— set status and headers in one call.res.setHeader(name, value)— set a single header.res.write(chunk)— write to the body. Multiple calls allowed.res.end([chunk])— finalize and close. Required.
Forgetting res.end() means the client hangs forever. The framework you usually use wraps this contract and ensures end is called; the http module does not police you.
Reading a Request Body
import { createServer } from 'node:http';
import { json } from 'node:stream/consumers';
createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/api/users') {
try {
const body = await json(req); // consume + parse in one helper
console.log('got user:', body.name);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 42, ...body }));
} catch (e) {
res.writeHead(400);
res.end('bad json');
}
}
}).listen(3000);The node:stream/consumers module (Node 16+) provides text(), json(), buffer(), arrayBuffer(), blob() — each fully consumes a stream into the named shape. Replaces the manual chunk-accumulation dance that every Node tutorial used to teach.Outbound HTTP — The Old Way
The http module also has a client side via http.request / http.get. In 2026 nobody uses these for HTTP clients — fetch (next lesson) is the universal answer. The exception is when you need precise control over the underlying socket: keep-alive policies, custom DNS resolution, raw stream access. For 99% of outbound HTTP, fetch is right.
HTTPS — Same API, Different Module
node:https is structurally identical to node:http but adds TLS. createServer(options, handler) takes { key, cert } for the server's certificate. Most production deployments terminate TLS at a reverse proxy (nginx, Cloudflare, the cloud load balancer) and serve plain http internally — fewer cert-rotation headaches, easier process restarts. Decide where TLS lives in your stack on day one.