Stop 02 · Node.js FundamentalsProjects · 3/3

Simple HTTP Server (No Framework)

Part of the Backend Developer Roadmap

In short

Express hides a lot. Building a server with plain node:http shows you what's underneath: raw URLs, body chunks, status codes and headers. After this, frameworks stop feeling like magic.

A basic web server built with only Node's built-in `http` module — before reaching for Express, understand what a framework is actually doing for you.

The mascot runs a wooden vegetable cart alone, weighing tomatoes and handing a bag to a customer.

Think of it like this

Running your own sabzi thela

No shop, no staff, no billing machine. You call out prices, weigh the tomatoes, pack the bag and take the money yourself. It's more work, but you learn every step of the business. A server without a framework is exactly that.

Watch it happen

Talking to a bare node:http server

curl
Your server
Memory

GET /notes

1/8A request arrives. You get req.method and req.url as plain strings and route on them yourself.

The big ideas

What you need to know

Routing is if-statements

Match req.method and the URL's pathname yourself. That's all a router really does.

The body is a stream

Request bodies arrive in chunks. Read them with for await, then parse when the stream ends.

You set every header

Status, Content-Type and the body are all your job, through writeHead and end.

Handle the bad cases

Invalid JSON gets a 400, unknown routes a 404, and huge bodies should be cut off with a 413.

See it in code

Walk through the code

js
1import http from "node:http";2const notes = [];3const send = (res, status, data) =>4  res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(data));5http.createServer(async (req, res) => {6  const { pathname } = new URL(req.url, "http://localhost");7  if (req.method === "GET" && pathname === "/notes") return send(res, 200, notes);8  if (req.method === "POST" && pathname === "/notes") {9    req.setEncoding("utf8");10    let body = "";11    for await (const chunk of req) body += chunk;12    try { notes.push(JSON.parse(body)); return send(res, 201, notes.at(-1)); }13    catch { return send(res, 400, { error: "Invalid JSON" }); }14  }15  send(res, 404, { error: "Not found" });16}).listen(3000, () => console.log("http://localhost:3000"));

1/4A tiny helper: status code, JSON header and body in one go. writeHead returns res, so end() chains on.

Test with curl -X POST localhost:3000/notes -d '{"text":"hi"}' and then curl localhost:3000/notes. setEncoding makes sure characters split across chunks are joined correctly.

Save yourself the pain

Common mistakes

Oops

Forgetting to call res.end() on one of the paths, so the client hangs.

Do this instead

Every branch must end the response. A catch-all 404 at the bottom makes sure nothing slips through.

Oops

Reading request bodies of any size into memory.

Do this instead

A huge body can exhaust memory. Count bytes as chunks arrive and reply 413 once you pass a limit.

Your mission

First milestone: a notes API

~40 min

Quick check

In a plain node:http server, why do you loop with for await (const chunk of req) to read a POST body?

Go deeper

  1. 1Build a server that routes different URLs to different responses manually.
  2. 2Parse the request body and query string without any helper libraries.
  3. 3Compare the amount of boilerplate this takes versus the same server in Express.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge