HTTP & Building a Server From Scratch (Using Core http)
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.methodreq.urlreq.headersrequest 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– OK201– Created400– Bad request401– Unauthorized404– Not found500– 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.