An HTTP Server by Hand

Building an HTTP Server by Hand

http.createServer gives you a net.Server whose sockets are fed to llhttp, the C parser Node embeds. Once it has the request line and headers — before the body arrives — it builds an IncomingMessage (readable) and a ServerResponse (writable) and calls your listener.

A JSON endpoint with no frameworkJavaScript
import { createServer } from 'node:http';
createServer((req, res) => {
  const { pathname } = new URL(req.url, `http://${req.headers.host}`);
  if (req.method === 'GET' && pathname === '/books') {
    const body = JSON.stringify([{ id: 1, title: 'Node in Practice' }]);
    res.writeHead(200, { 'content-type': 'application/json; charset=utf-8',
                         'content-length': Buffer.byteLength(body) });
    return res.end(body);
  }
  res.writeHead(404, { 'content-type': 'text/plain' }).end('not found\n');
}).listen(3000, '127.0.0.1');
Output
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 37
Date: Thu, 17 Sep 2026 11:28:55 GMT
Connection: keep-alive
Keep-Alive: timeout=5
[{"id":1,"title":"Node in Practice"}]

That is the reply to curl 3,008 -i localhost:3000/books. req.url is a path with a query string, never an absolute URL, so URL needs a base. req.headers keys are lowercased and repeats are joined with a comma — except set-cookie, always an array. Content-Length uses Buffer.byteLength, since a character is not a byte in UTF-8.