← thecodex.expert · The Codex Family of Knowledge
The Codex / JavaScript / http — HTTP Server

http — HTTP Server

Building HTTP servers in Node.js without frameworks — createServer, req/res objects, routing, status codes, and HTTPS.

What it is, in plain English: The http module (built into Node.js) lets you create HTTP servers from scratch. http.createServer() takes a callback that receives every request. The req object contains the method, URL, headers, and body. The res object sends the response — status code, headers, and body. This is what every web framework (Express, Fastify, Hono) wraps under the hood.
Where this bites you

Body data arrives as Buffer chunks, and a chunk boundary can fall in the middle of a multi-byte character such as ₹ (3 bytes in UTF-8) or Devanagari letters. body += chunk converts each chunk to a string on its own, so a character split between two chunks turns into � replacement marks. English-only test data never shows it.

import { createServer, request } from "http";

// a server whose response is split mid-character, as TCP chunks can be:
const server = createServer((req, res) => {
  const bytes = Buffer.from('{"price":"₹499","city":"मुंबई"}');
  res.write(bytes.subarray(0, 11));            // ends in the middle of the 3-byte "₹"
  setTimeout(() => res.end(bytes.subarray(11)), 20);
}).listen(0, () => {
  const req = request(`http://localhost:${server.address().port}/`, (res) => {
    let body = "";
    res.on("data", chunk => body += chunk);    // each Buffer chunk decoded on its own
    res.on("end", () => { console.log(body); server.close(); });
  });
  req.end();
});
// {"price":"���499","city":"मुंबई"}

Your first HTTP server without a framework

Every web framework — Express, Fastify, Hono — sits on top of Node's built-in http module. Understanding the raw http module shows you what frameworks are actually doing.

The simplest possible server

import { createServer } from "http";

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

server.listen(3000, () => {
  console.log("Running at http://localhost:3000");
});

Reading the request

const server = createServer((req, res) => {
  console.log(req.method);   // "GET", "POST", "PUT", "DELETE" etc.
  console.log(req.url);      // "/api/users?page=2"
  console.log(req.headers);  // { "content-type": "application/json", ... }

  // Parse URL and query string:
  const url    = new URL(req.url, "http://localhost");
  console.log(url.pathname);         // "/api/users"
  console.log(url.searchParams.get("page")); // "2"
});

Sending responses

function sendJSON(res, statusCode, data) {
  res.writeHead(statusCode, {
    "Content-Type":                "application/json",
    "Access-Control-Allow-Origin": "*",   // CORS
  });
  res.end(JSON.stringify(data));
}

const server = createServer((req, res) => {
  if (req.method === "GET" && req.url === "/api/ping") {
    sendJSON(res, 200, { ok: true, ts: Date.now() });
    return;
  }
  sendJSON(res, 404, { error: "Not found" });
});

Reading the request body (POST/PUT)

// The body arrives as a stream — collect chunks:
function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on("data",  chunk => chunks.push(chunk));
    req.on("end",   ()    => resolve(Buffer.concat(chunks).toString()));
    req.on("error", reject);
  });
}

const server = createServer(async (req, res) => {
  if (req.method === "POST" && req.url === "/api/users") {
    try {
      const raw  = await readBody(req);
      const user = JSON.parse(raw);
      // ... save user ...
      sendJSON(res, 201, { ...user, id: Date.now() });
    } catch {
      sendJSON(res, 400, { error: "Invalid JSON body" });
    }
  }
});

The readBody() helper above gets this right: it keeps the raw Buffer chunks and decodes once, after Buffer.concat(). If you prefer working with strings, call res.setEncoding("utf8") (or req.setEncoding) first. Node then decodes across chunk boundaries for you, which also gave the correct output in testing:

import { createServer, request } from "http";

const server = createServer((req, res) => {
  const bytes = Buffer.from('{"price":"₹499","city":"मुंबई"}');
  res.write(bytes.subarray(0, 11));
  setTimeout(() => res.end(bytes.subarray(11)), 20);
}).listen(0, () => {
  const req = request(`http://localhost:${server.address().port}/`, (res) => {
    const chunks = [];
    res.on("data", chunk => chunks.push(chunk));                         // keep the raw bytes
    res.on("end", () => {
      console.log(Buffer.concat(chunks).toString("utf-8"));              // decode once, at the end
      server.close();
    });
  });
  req.end();
});
// {"price":"₹499","city":"मुंबई"}
Try It Yourself

Official Sources

The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.