Topic 5: Building a HTTP Server with Node.JS using HTTP APIs
📖 8 min read · 🎯 intermediate · 🧭 Prerequisites: using-rest, working-with-asynchronous-programming
Why this matters
Up until now, you've probably used tools or frameworks without really knowing what's happening underneath — and that's fine, until it isn't. Here's the thing — every web server you've ever visited, every API you've ever called, is doing one simple thing: receiving a request and sending back a response. In this lesson, we build that from scratch using Node.js and its built-in http module. No Express, no shortcuts. Just raw Node.js. Once you see how http.createServer() works at this level, every framework you touch after this will finally make sense.
What You'll Learn
- How to create a bare Node.js HTTP server using the built-in
httpmodule - How to handle multiple URL routes and return appropriate responses
- How to receive and parse POST request bodies as JSON
- How to serve static files (HTML, CSS, JS, images) with correct MIME types
The Analogy
Think of an HTTP server as Vizag's central post office. Every citizen (browser or API client) walks up to the counter and hands the clerk (your server) a letter describing what they want — the address on the envelope is the URL, and the stamp in the corner is the HTTP method (GET, POST, etc.). The clerk reads the address, finds the right department, and hands back a reply envelope containing the answer. If the address doesn't exist, the clerk stamps it "404 Not Found" and sends it back. Node.js's http module puts you behind that counter — you decide exactly how every envelope gets sorted and answered.
Chapter 1: Introduction to Node.js HTTP APIs
Node.js ships with a built-in http module — no npm install required. It gives you everything you need to open a TCP socket, accept connections, and speak the HTTP protocol.
Three core concepts cover everything:
- Creating a server —
http.createServer(callback)returns aServerobject. The callback receives areq(IncomingMessage) andres(ServerResponse) on every incoming request. - Listening for requests —
server.listen(port, callback)binds the server to a port. The callback fires once the socket is ready. - Handling requests and responses — Inside the callback you inspect
req.url,req.method, andreq.headers, then useres.writeHead(),res.setHeader(), andres.end()to build a reply.
sequenceDiagram
participant Client
participant Node HTTP Server
participant Callback
Client->>Node HTTP Server: HTTP Request (method + URL + headers + body)
Node HTTP Server->>Callback: req, res objects
Callback->>Callback: Route logic
Callback->>Client: res.writeHead() + res.end()
Chapter 2: Setting Up a Basic HTTP Server
The class decided to start with the simplest possible server — one that replies "Hello, Vizag!" to every request.
Step 1: Create a new Node.js project
mkdir node-http-server
cd node-http-server
npm init -y
Step 2: Create server.js
const http = require('http');
// Create an HTTP server
const server = http.createServer((req, res) => {
// Set the response HTTP header with HTTP status and Content-Type
res.writeHead(200, { 'Content-Type': 'text/plain' });
// Send the response body
res.end('Hello, Vizag!\n');
});
// Listen for incoming requests on port 3000
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
Step 3: Run the server
node server.js
Navigating to http://localhost:3000/ in a web browser displays Hello, Vizag!.
Chapter 3: Handling Different Routes
A real server returns different content for different URLs. the trainer showed the class how to inspect req.url and branch accordingly.
server.js — route handling:
const http = require('http');
const server = http.createServer((req, res) => {
// Parse the URL
const url = req.url;
// Set the response HTTP header with HTTP status and Content-Type
res.writeHead(200, { 'Content-Type': 'text/plain' });
// Route handling
if (url === '/') {
res.end('Welcome to Vizag!\n');
} else if (url === '/about') {
res.end('About Vizag\n');
} else if (url === '/contact') {
res.end('Contact Vizag\n');
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found\n');
}
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
Navigating to /, /about, or /contact shows the corresponding message. Any other path returns 404 Not Found.
Key takeaway: res.writeHead() can be called again after the first call to override the status code — just make sure you call it before res.end().
Chapter 4: Handling POST Requests
GET routes are straightforward, but POST requests carry a body that arrives in chunks. the trainer introduced the class to Node.js streams to collect those chunks before parsing JSON.
server.js — POST request handling:
const http = require('http');
// Helper function to collect and parse JSON from the request stream
const parseRequestBody = (req) => {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
resolve(JSON.parse(body));
} catch (error) {
reject(error);
}
});
});
};
const server = http.createServer(async (req, res) => {
const url = req.url;
// Set default response headers
res.setHeader('Content-Type', 'application/json');
// Route handling
if (url === '/submit' && req.method === 'POST') {
try {
const body = await parseRequestBody(req);
res.writeHead(200);
res.end(JSON.stringify({ message: 'Data received', data: body }));
} catch (error) {
res.writeHead(400);
res.end(JSON.stringify({ message: 'Invalid JSON' }));
}
} else {
res.writeHead(404);
res.end(JSON.stringify({ message: '404 Not Found' }));
}
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
Testing the POST endpoint with cURL:
curl -X POST http://localhost:3000/submit \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": 30}'
Response:
{
"message": "Data received",
"data": {
"name": "Alice",
"age": 30
}
}
The class also tested this with Postman — both tools are valid for hitting the /submit endpoint manually.
How parseRequestBody works:
req.on('data', chunk => ...)— fires each time a chunk of body data arrives; concatenated intobody.req.on('end', () => ...)— fires when all chunks have arrived; parse the full string as JSON.- Wrapping in a
Promiselets theasyncserver callbackawaitit cleanly.
Chapter 5: Serving Static Files
the trainer saved the most practical chapter for last: serving real HTML, CSS, JavaScript, and image files from disk — the backbone of any traditional web server.
server.js — static file server:
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
// Default to index.html when root is requested
const url = req.url === '/' ? '/index.html' : req.url;
const filePath = path.join(__dirname, 'public', url);
// Read the file from disk
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found\n');
} else {
// Determine Content-Type from file extension
const ext = path.extname(filePath);
let contentType = 'text/plain';
switch (ext) {
case '.html':
contentType = 'text/html';
break;
case '.css':
contentType = 'text/css';
break;
case '.js':
contentType = 'application/javascript';
break;
case '.json':
contentType = 'application/json';
break;
case '.png':
contentType = 'image/png';
break;
case '.jpg':
contentType = 'image/jpg';
break;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
}
});
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
Create the public directory and add index.html:
mkdir public
public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vizag</title>
</head>
<body>
<h1>Welcome to Vizag!</h1>
<p>This is the home page.</p>
</body>
</html>
Navigating to http://localhost:3000/ in a web browser displays the full HTML content of index.html.
Why the MIME type switch matters: Browsers use Content-Type to decide how to render a response. Sending HTML with text/plain displays the raw markup as text instead of rendering it. The switch on path.extname() maps each file type to its correct MIME type.
🧪 Try It Yourself
Task: Extend the route-handling server from Chapter 3 to also support a GET /time endpoint that returns the current server timestamp as JSON.
Success criterion: Running curl http://localhost:3000/time should print something like:
{ "time": "2026-05-21T10:30:00.000Z" }
Starter snippet — add this branch inside your http.createServer callback:
} else if (url === '/time' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ time: new Date().toISOString() }));
}
Paste it before the else (404) branch, restart the server with node server.js, and hit the endpoint with cURL or your browser.
🔍 Checkpoint Quiz
Q1. What is the purpose of res.writeHead() in a Node.js HTTP server?
A) It reads incoming request headers
B) It sets the HTTP status code and response headers before sending the body
C) It terminates the response stream
D) It parses the URL from the request
Q2. Given this snippet, what would a browser receive when it visits http://localhost:3000/about?
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
if (req.url === '/') {
res.end('Home');
} else if (req.url === '/about') {
res.end('About Vizag');
} else {
res.writeHead(404);
res.end('Not Found');
}
});
A) Home
B) About Vizag
C) Not Found
D) An error — res.writeHead was already called
Q3. In the parseRequestBody helper, why does the code listen to the 'data' event instead of reading the body all at once?
A) The http module does not support synchronous reads
B) HTTP request bodies arrive as a stream of chunks, which must be accumulated
C) Node.js requires the fs module to read request bodies
D) The 'data' event fires only once with the full body
Q4. You are building a Node.js HTTP server that must return a PNG image at /logo.png. Which Content-Type header value should you set, and where in the static file server's switch statement does it belong?
(Open-ended — write the case block.)
A1. B — res.writeHead() sets the HTTP status code and any response headers (like Content-Type) that will be sent before the body. It must be called before res.end().
A2. B — req.url matches /about, so the server calls res.end('About Vizag'). The second res.writeHead(404) is inside the else branch, which is never reached.
A3. B — HTTP bodies are transmitted as a stream. Node.js exposes incoming data through the 'data' event one chunk at a time; the 'end' event signals that all chunks have arrived and the full body can be parsed.
A4.
case '.png':
contentType = 'image/png';
break;
Add this inside the switch (ext) block. The Content-Type value image/png tells the browser to render the response as a PNG image rather than as text or an unknown binary blob.
🪞 Recap
- Node.js's built-in
httpmodule lets you create a fully functional HTTP server with zero external dependencies usinghttp.createServer()andserver.listen(). - Route logic lives inside the server callback — inspect
req.urlandreq.methodto branch into different handlers. - POST bodies arrive as a stream of chunks; collect them via the
'data'event and parse on'end'. - Static files are served by reading them from disk with
fs.readFile()and mapping file extensions to correct MIME types viaContent-Type. - Every response needs
res.writeHead()(status + headers) followed byres.end()(body) — missing either leaves the client hanging.
📚 Further Reading
- Node.js
httpmodule — official docs — the source of truth on every method and event covered here - MDN HTTP overview — understand the protocol your server is implementing
- MIME types reference — MDN — complete list of Content-Type values for serving any file format
- ⬅️ Previous: Working with Asynchronous Programming
- ➡️ Next: Joint Query, Nested Query & Filtering Data