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.

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
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
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
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
- 1Build a server that routes different URLs to different responses manually.
- 2Parse the request body and query string without any helper libraries.
- 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.