Skip to content
Node.js core 3 min read

Building an HTTP Server

Before frameworks, there is the http module—Node’s built-in, dependency-free way to serve the web. Understanding it demystifies everything Express does for you and equips you to build lightweight services where a framework would be overkill.

The http module

http.createServer takes a handler called once per request, receiving a request stream and a response stream.

const http = require("http");

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/plain");
  res.end("Hello, world");
});

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

The callback runs for every incoming request. The server keeps listening until you stop it—a single process handling many concurrent connections via the event loop.

Request and response

The req object exposes the incoming message; res is how you reply. Key members:

ObjectPropertyPurpose
reqreq.urlPath and query string, e.g. /users?id=1
reqreq.methodGET, POST, etc.
reqreq.headersIncoming headers object
resres.statusCodeNumeric status to send
resres.setHeader()Set a response header
resres.end()Finish and flush the response

req is a readable stream and res is a writable stream. The body doesn’t arrive all at once—you read it in chunks.

Routing by URL and method

The core module has no router, so you branch on req.method and req.url yourself:

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/health") {
    res.statusCode = 200;
    return res.end("OK");
  }

  if (req.method === "GET" && req.url === "/users") {
    res.setHeader("Content-Type", "application/json");
    return res.end(JSON.stringify([{ id: 1, name: "Ada" }]));
  }

  res.statusCode = 404;
  res.end("Not Found");
});

server.listen(3000);

Serving JSON

JSON is the lingua franca of APIs. Set the right header and serialize your data:

function sendJson(res, status, payload) {
  res.statusCode = status;
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify(payload));
}

sendJson(res, 201, { id: 2, name: "Grace" });

Reading a request body

POST and PUT bodies arrive as stream chunks. Collect them, then parse:

function readBody(req) {
  return new Promise((resolve, reject) => {
    let data = "";
    req.on("data", (chunk) => (data += chunk));
    req.on("end", () => resolve(data ? JSON.parse(data) : {}));
    req.on("error", reject);
  });
}

Status codes that matter

CodeMeaningUse when
200OKSuccessful GET/PUT
201CreatedA resource was created
204No ContentSuccessful DELETE
400Bad RequestInvalid client input
404Not FoundUnknown route or resource
500Server ErrorUnexpected failure

Streams basics

Because res is a writable stream, you can pipe large files without loading them into memory—critical for big payloads:

const fs = require("fs");
const http = require("http");

http
  .createServer((req, res) => {
    res.setHeader("Content-Type", "video/mp4");
    fs.createReadStream("./movie.mp4").pipe(res);
  })
  .listen(3000);

pipe reads the file in chunks and writes each to the response as it goes, applying backpressure so a slow client never overwhelms memory.

Best Practices

  • Always set Content-Type so clients parse responses correctly.
  • Wrap body parsing in try/catch—never trust client JSON.
  • Stream large files with pipe instead of buffering them.
  • Handle the error event on requests and the server to avoid crashes.
  • Return precise status codes; they’re part of your API contract.
  • For anything beyond a few routes, graduate to Express—covered next.
Last updated June 1, 2026
Was this helpful?