Skip to main content

Command Palette

Search for a command to run...

HTTP & Building a Server From Scratch (Using Core http)

Updated
3 min read

if you really want to understand how servers work, you need to go one level lower. You need to touch the raw metal. You need to build a server using the core http module. That’s what this post is about.

In this we are gonna -

  • Understand what HTTP actually is

  • Build a server from scratch

  • Parse routes manually

  • Read headers

  • Handle request bodies using streams

  • Send proper status codes

what is HTTP ?

Starting with the most important/basic thing, what http is actually ? HTTP is just a conversation protocol.

Every HTTP request has:

  • Method (GET, POST, PUT, etc etc)

  • URL

  • Headers

  • Body (optional)

Every HTTP response has:

  • Status code (200, 404, 500…)

  • Headers

  • Body

Creating the smallest possible HTTP server

starting with most basic one

const http = require("http");

const server = http.createServer((req, res) => {
  res.end("Hello World");
});

server.listen(3000, () => {
  console.log("Server running on port 3000");
});

Understanding req and res

req (Incoming Message)

And it mostly contains -

  • req.method

  • req.url

  • req.headers

  • request body as a stream

res (Server response)

It is Used to -

  • set status codes

  • set headers

  • send response body

Routing in nodejs

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

  if (req.method === "GET" && req.url === "/health") {
    res.end("OK");
    return;
  }

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

Routing is just if-else on method and path(i love this analogy, got it from a cohort and still carry with me). Every framework eventually does this just in a cleaner way.

Reading request headers

Headers are metadata.

Auth tokens, content type, user agent all comes through headers.

const server = http.createServer((req, res) => {
  console.log(req.headers);

  res.end("Check your terminal");
});
/* output you'll see in terminal. it might differ a bit
content-type
authorization
user-agent
content-length
*/

Handling request body

Request body is a stream. The body does not come all at once. It arrives in chunks.

const server = http.createServer((req, res) => {
  let body = "";

  req.on("data", (chunk) => {
    body += chunk;
  });

  req.on("end", () => {
    console.log(body);
    res.end("Body received");
  });
});

This pattern matters a lot in real systems. Because -

  • Requests can be huge

  • Streaming avoids loading everything in memory

  • This scales better

Parsing JSON request body

req.on("end", () => {
  try {
    const data = JSON.parse(body);
    console.log(data);
    res.end("JSON parsed");
  } catch (err) {
    res.statusCode = 400;
    res.end("Invalid JSON");
  }
});

On eimportant thing to notice here is that you are responsible for validations here, frameworks just hide these from us.

Sending proper status codes

res.statusCode = 201;
res.end("User created");

Some common status codes with meaning

  • 200 – OK

  • 201 – Created

  • 400 – Bad request

  • 401 – Unauthorized

  • 404 – Not found

  • 500 – Server error

Setting response headers

Headers go out before body

res.setHeader("Content-Type", "application/json");
res.statusCode = 200;
res.end(JSON.stringify({ message: "Hello" }));

Order doesn’t matter in code that much. Node sends them correctly under the hood.

A tiny but complete server

Putting everything together

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.method === "POST" && req.url === "/echo") {
    let body = "";

    req.on("data", (chunk) => {
      body += chunk;
    });

    req.on("end", () => {
      res.setHeader("Content-Type", "application/json");
      res.statusCode = 200;
      res.end(body);
    });

    return;
  }

  res.statusCode = 404;
  res.end("Route not found");
});

server.listen(3000);

Once we understand these foundations of a server Frameworks stop feeling magical. Debugging becomes easier. Performance issues make sense.