Topic 5 of 18 · Node Expert

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 bee...
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 been writing Node.js code that runs and exits — scripts that do their job and stop. But real applications don't stop. They wait. A web server sits there, listening for someone to knock, and when they do, it responds. That's what we're building today — your first HTTP server using Node.js's built-in http module, no frameworks, no shortcuts. Just raw Node.js so you can see exactly what's happening when a browser sends a request and your code sends something back.

What You'll Learn

  • Create a basic HTTP server using Node.js's built-in http module
  • Handle multiple URL routes and return appropriate responses
  • Process incoming POST request bodies by parsing streamed JSON data
  • Serve static files (HTML, CSS, JS, images) with correct Content-Type headers
  • Test HTTP endpoints with cURL and browser navigation

The Analogy

Think of an HTTP server as a hotel concierge desk. Every guest (HTTP request) walks through the front door carrying a note that says where they want to go (req.url) and what they want to do (GET, POST, etc.). The concierge (your request handler) reads the note, consults the right department, and hands back a response — a room key, a map, or a polite "404: that room doesn't exist." The desk never closes (server.listen), always waits for the next guest, and handles one note at a time without losing track of others. Your Node.js server is that desk: always on, always reading, always responding.

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 stand up a functioning web server with three core interactions:

  1. Creating a Serverhttp.createServer() registers a callback that fires on every incoming request.
  2. Listening for Requestsserver.listen() binds the server to a port and starts accepting connections.
  3. Handling Requests and Responses — the callback receives two objects:
    • req (IncomingMessage) — the incoming request: URL, method, headers, body stream.
    • res (ServerResponse) — the outgoing response: status code, headers, body.
sequenceDiagram
    participant Browser
    participant Node as Node.js HTTP Server
    Browser->>Node: HTTP Request (method, url, headers, body)
    Node->>Node: http.createServer callback fires
    Node->>Browser: HTTP Response (statusCode, headers, body)

Key response methods you'll use throughout this lesson:

MethodPurpose
res.writeHead(statusCode, headers)Write status line + headers
res.setHeader(name, value)Set a single header before writing
res.end(body)Send body and close the response

Chapter 2: Setting Up a Basic HTTP Server

Step 1: Create a New Node.js Project

mkdir node-http-server
cd node-http-server
npm init -y

Step 2: Create the Server File

Create a file named server.js with the following contents.

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 "Hello, Vizag!"
    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!".

Every request — regardless of URL or method — currently hits the same handler. The next chapter fixes that by inspecting req.url.

Chapter 3: Handling Different Routes

Real servers need to respond differently depending on which path was requested. the trainer showed the class how to branch on req.url.

server.js:

const http = require('http');

const server = http.createServer((req, res) => {
    // Parse the URL
    const url = req.url;

    // Route handling
    if (url === '/') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Welcome to Vizag!\n');
    } else if (url === '/about') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('About Vizag\n');
    } else if (url === '/contact') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Contact Vizag\n');
    } else {
        res.writeHead(404, { 'Content-Type': 'text/plain' });
        res.end('404 Not Found\n');
    }
});

// Listen for incoming requests on port 3000
const PORT = 3000;
server.listen(PORT, () => {
    console.log(`Server running at http://localhost:${PORT}/`);
});

What each route does:

  • /"Welcome to Vizag!" with status 200
  • /about"About Vizag" with status 200
  • /contact"Contact Vizag" with status 200
  • Anything else → "404 Not Found" with status 404

Notice that res.writeHead must be called before res.end. Calling writeHead after you've already started writing the body throws an error.

Chapter 4: Handling POST Requests

GET requests carry all their data in the URL. POST requests carry data in the request body — a readable stream that arrives in chunks. the trainer introduced a helper function to collect those chunks and parse them as JSON.

server.js:

const http = require('http');

// Helper function to parse JSON data from request
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) => {
    // Parse the URL
    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' }));
    }
});

// Listen for incoming requests on port 3000
const PORT = 3000;
server.listen(PORT, () => {
    console.log(`Server running at http://localhost:${PORT}/`);
});

Key details:

  • The http.createServer callback is declared async so you can await parseRequestBody.
  • req.on('data', ...) fires for each chunk; concatenating to a string works for JSON payloads.
  • req.on('end', ...) fires when the full body has arrived — that's when you parse.
  • A malformed JSON body triggers JSON.parse to throw, caught and returned as a 400.
  • req.method lets you gate a route to only accept specific HTTP verbs.

Testing the POST Endpoint

Use cURL to test the /submit endpoint:

curl -X POST http://localhost:3000/submit \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "age": 30}'

Expected response:

{
    "message": "Data received",
    "data": {
        "name": "Alice",
        "age": 30
    }
}

You can also test with Postman: set method to POST, URL to http://localhost:3000/submit, body to rawJSON, and paste the same payload.

Chapter 5: Serving Static Files

A server that can only return strings isn't much of a web server. the trainer showed the class how to read files from disk and stream them back with the correct Content-Type header — turning Node.js into a static file host.

server.js:

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer((req, res) => {
    // Parse the URL — default to index.html for root
    const url = req.url === '/' ? '/index.html' : req.url;
    const filePath = path.join(__dirname, 'public', url);

    // Read the file
    fs.readFile(filePath, (err, data) => {
        if (err) {
            res.writeHead(404, { 'Content-Type': 'text/plain' });
            res.end('404 Not Found\n');
        } else {
            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);
        }
    });
});

// Listen for incoming requests on port 3000
const PORT = 3000;
server.listen(PORT, () => {
    console.log(`Server running at http://localhost:${PORT}/`);
});

Content-Type map at a glance:

ExtensionContent-Type
.htmltext/html
.csstext/css
.jsapplication/javascript
.jsonapplication/json
.pngimage/png
.jpgimage/jpg

Creating the Public Directory

Create a public/ directory next to server.js and add an index.html:

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/ now renders the full HTML page. Add style.css or app.js inside public/ and the server will serve them automatically, using the switch statement to pick the right Content-Type.

Project layout:

node-http-server/
├── server.js
└── public/
    └── index.html

🧪 Try It Yourself

Task: Extend the static file server from Chapter 5 to also handle a /api/status GET route that returns a JSON object { "status": "ok", "city": "Vizag" } — without breaking static file serving for anything else.

Success criterion: Running the server and hitting http://localhost:3000/api/status with cURL or a browser should print:

{ "status": "ok", "city": "Vizag" }

While http://localhost:3000/ should still serve public/index.html as HTML.

Starter snippet — add this block to the http.createServer callback before the fs.readFile call:

if (req.url === '/api/status' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok', city: 'Vizag' }));
    return; // stop here — don't fall through to file serving
}

🔍 Checkpoint Quiz

Q1. What does res.writeHead(404, { 'Content-Type': 'text/plain' }) do, and why must it be called before res.end()?

Q2. Given this snippet, what does a POST request to /submit with body {"x": 1} print to the console? What does a GET request to /submit return?

const server = http.createServer(async (req, res) => {
    res.setHeader('Content-Type', 'application/json');
    if (req.url === '/submit' && req.method === 'POST') {
        const body = await parseRequestBody(req);
        console.log(body);
        res.writeHead(200);
        res.end(JSON.stringify({ ok: true }));
    } else {
        res.writeHead(404);
        res.end(JSON.stringify({ message: 'Not Found' }));
    }
});

A) Logs {"x": 1} / returns {"ok": true} for POST; returns {"message": "Not Found"} for GET
B) Logs {"x": 1} / returns {"ok": true} for both methods
C) Throws because parseRequestBody isn't defined
D) Logs undefined / crashes the server

Q3. In the static file server, why does the route check req.url === '/' and replace it with '/index.html' before building filePath?

A) Because browsers never send / as a URL
B) Because path.join fails on /
C) Because there is no file literally named / on disk — we need to map the root path to an actual file
D) Because fs.readFile requires absolute paths

Q4. You're building a Node.js HTTP server without any framework. A client sends a POST request with a large JSON body (~2 MB). How should you collect and parse that body safely, and what risk does concatenating chunks into a string introduce for very large payloads?

A1. res.writeHead writes the HTTP status code (404 = Not Found) and the Content-Type response header. It must be called before res.end() because HTTP requires headers to be sent before the body — once you start writing the body the header section is closed and any subsequent writeHead call throws a "Can't set headers after they are sent" error.

A2. A — A POST to /submit logs { x: 1 } (the parsed body object) and returns {"ok":true} with status 200. A GET to /submit falls into the else branch and returns {"message":"Not Found"} with status 404. parseRequestBody is assumed defined as in the lesson; the snippet itself doesn't redefine it, but the question states the full lesson context.

A3. C — The root URL / doesn't correspond to a file on disk. path.join(__dirname, 'public', '/') resolves to the public directory itself, not a file. Rewriting it to /index.html maps the root request to the actual public/index.html file so fs.readFile can read it.

A4. Collect chunks with req.on('data', chunk => body += chunk.toString()) and parse in req.on('end', ...). The risk: concatenating string chunks keeps the entire body in memory at once. For very large payloads this can exhaust heap memory. A safer approach is to enforce a size limit (reject the request if accumulated length exceeds a threshold) or use a streaming JSON parser. For typical API payloads under a few MB, the string-concat approach shown in the lesson is fine.

🪞 Recap

  • Node.js's built-in http module lets you create a server with http.createServer() and bind it to a port with server.listen() — no framework required.
  • Every request fires the same callback; route by inspecting req.url and gate verbs with req.method.
  • POST bodies arrive as a stream of chunks — collect them with req.on('data', ...), parse in req.on('end', ...), and wrap the whole thing in a Promise for clean async/await usage.
  • Serve static files by reading from disk with fs.readFile and mapping file extensions to Content-Type headers so browsers render them correctly.
  • Always call res.writeHead before res.end; mixing up the order causes a "headers already sent" error.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

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