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:
| Object | Property | Purpose |
|---|---|---|
req | req.url | Path and query string, e.g. /users?id=1 |
req | req.method | GET, POST, etc. |
req | req.headers | Incoming headers object |
res | res.statusCode | Numeric status to send |
res | res.setHeader() | Set a response header |
res | res.end() | Finish and flush the response |
reqis a readable stream andresis 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
| Code | Meaning | Use when |
|---|---|---|
| 200 | OK | Successful GET/PUT |
| 201 | Created | A resource was created |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid client input |
| 404 | Not Found | Unknown route or resource |
| 500 | Server Error | Unexpected 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-Typeso clients parse responses correctly. - Wrap body parsing in
try/catch—never trust client JSON. - Stream large files with
pipeinstead of buffering them. - Handle the
errorevent 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.