Topic 20 of 56 · Full Stack Advanced

Topic 5 : Building a HTTP Server with Node.JS using HTTP APIs

Lesson TL;DRTopic 5: Building a HTTP Server with Node.JS using HTTP APIs 📖 8 min read · 🎯 intermediate · 🧭 Prerequisites: usingrest, workingwithasynchronousprogramming Why this matters Up until now, you've pro...
8 min read·intermediate·nodejs · http · server · routing

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 http module
  • 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:

  1. Creating a serverhttp.createServer(callback) returns a Server object. The callback receives a req (IncomingMessage) and res (ServerResponse) on every incoming request.
  2. Listening for requestsserver.listen(port, callback) binds the server to a port. The callback fires once the socket is ready.
  3. Handling requests and responses — Inside the callback you inspect req.url, req.method, and req.headers, then use res.writeHead(), res.setHeader(), and res.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 into body.
  • req.on('end', () => ...) — fires when all chunks have arrived; parse the full string as JSON.
  • Wrapping in a Promise lets the async server callback await it 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 http module lets you create a fully functional HTTP server with zero external dependencies using http.createServer() and server.listen().
  • Route logic lives inside the server callback — inspect req.url and req.method to 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 via Content-Type.
  • Every response needs res.writeHead() (status + headers) followed by res.end() (body) — missing either leaves the client hanging.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.