02. HTTP Server

In this module, you'll build a web server from scratch using Node's built-in http module. This shows you what frameworks like Express abstract away.

Demo Files

Run locally: node basic-server.js then visit http://localhost:3000

Try it live: cse135.site:3001/demo - A raw Node.js server running on this site

The Key Insight

In PHP, Apache is the server and PHP runs inside it. In Node.js, your code IS the server:

PHP: Node.js: ┌──────────────┐ ┌──────────────────────┐ │ Browser │ │ Browser │ └──────┬───────┘ └──────────┬───────────┘ │ │ ▼ │ ┌──────────────┐ │ │ Apache │ ◄── Web server │ │ (mod_php) │ │ └──────┬───────┘ ▼ │ ┌──────────────────────┐ ▼ │ Your Node.js Code │ ┌──────────────┐ │ │ │ Your PHP │ │ http.createServer() │ │ Code │ │ │ └──────────────┘ └──────────────────────┘ ▲ │ YOU write the server!

Your First HTTP Server

// basic-server.js
const http = require('http');

const server = http.createServer((req, res) => {
    // This function runs for EVERY request
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World!');
});

server.listen(3000, () => {
    console.log('Server running at http://localhost:3000/');
});

Run it:

$ node basic-server.js
Server running at http://localhost:3000/

Open http://localhost:3000 in your browser. You'll see "Hello World!"

The server runs continuously. Unlike PHP scripts that exit after each request, your Node.js server keeps running, waiting for requests. Press Ctrl+C to stop it.

Understanding Request and Response

The callback function receives two objects:

The Request Object (req)

// Basic request info
req.url        // '/about' or '/users?id=5'
req.method     // 'GET', 'POST', 'PUT', 'DELETE'
req.headers    // { 'content-type': '...', 'user-agent': '...' }

Reading Headers

Request headers are available as a lowercase key object:

// Access specific headers
const userAgent = req.headers['user-agent'];
const contentType = req.headers['content-type'];
const acceptLang = req.headers['accept-language'];
const host = req.headers['host'];

// Get client IP (may be behind proxy)
const clientIP = req.headers['x-forwarded-for'] ||
                 req.connection.remoteAddress;

Parsing Query Strings

The req.url includes the query string. Use the built-in url module to parse it:

const url = require('url');

const server = http.createServer((req, res) => {
    // Parse URL: '/search?q=nodejs&page=2'
    const parsedUrl = url.parse(req.url, true);

    const pathname = parsedUrl.pathname;  // '/search'
    const query = parsedUrl.query;        // { q: 'nodejs', page: '2' }

    console.log(`Searching for: ${query.q}, page ${query.page}`);
});
Compare to PHP: In PHP, $_GET['q'] gives you query parameters instantly. In Node.js, you must parse the URL yourself. Frameworks like Express provide req.query for convenience.

The Response Object (res)

// Methods to send the response
res.writeHead(200, { 'Content-Type': 'text/html' });  // Set status and headers
res.write('Some content');                             // Write to body (can call multiple times)
res.end('Final content');                              // End the response

Manual Routing

Without a framework, you handle routing manually by checking req.url:

// routing.js
const http = require('http');

const server = http.createServer((req, res) => {
    // Check the URL and method
    if (req.url === '/' && req.method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end('<h1>Home Page</h1>');
    }
    else if (req.url === '/about' && req.method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end('<h1>About Page</h1>');
    }
    else if (req.url === '/api/data' && req.method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ message: 'Hello from API' }));
    }
    else {
        res.writeHead(404, { 'Content-Type': 'text/html' });
        res.end('<h1>404 Not Found</h1>');
    }
});

server.listen(3000);
This is tedious! Imagine handling 50 routes this way. That's why frameworks like Express exist. But understanding this helps you appreciate what Express does for you.

Serving Static Files

To serve HTML, CSS, and image files, you need to read them from disk:

// static-files.js
const http = require('http');
const fs = require('fs');
const path = require('path');

// Map file extensions to content types
const mimeTypes = {
    '.html': 'text/html',
    '.css': 'text/css',
    '.js': 'application/javascript',
    '.json': 'application/json',
    '.png': 'image/png',
    '.jpg': 'image/jpeg'
};

const server = http.createServer((req, res) => {
    // Build file path (default to index.html)
    let filePath = '.' + req.url;
    if (filePath === './') filePath = './index.html';

    // Get the file extension
    const ext = path.extname(filePath);
    const contentType = mimeTypes[ext] || 'application/octet-stream';

    // Read and serve the file
    fs.readFile(filePath, (err, content) => {
        if (err) {
            if (err.code === 'ENOENT') {
                res.writeHead(404);
                res.end('File not found');
            } else {
                res.writeHead(500);
                res.end('Server error');
            }
        } else {
            res.writeHead(200, { 'Content-Type': contentType });
            res.end(content);
        }
    });
});

server.listen(3000);

This is a lot of code just to serve files. Express does this in one line:

app.use(express.static('public'));  // That's it!
You're Rebuilding Apache: Notice how we're manually building MIME type mappings? This is exactly what Apache does for you automatically via its mime.types configuration. And this code is still missing many things Apache handles out of the box:

This illustrates Node.js's trade-off: it's less "batteries included" and more DIY. This can be good for performance (you only include what you need) and limiting attack surface (less code = fewer vulnerabilities). But it's a double-edged sword: you can easily forget things that Apache handles automatically.

Reading the Request Body (POST Data)

POST data arrives in chunks. You must collect them:

const server = http.createServer((req, res) => {
    if (req.method === 'POST') {
        let body = '';

        // Collect data chunks
        req.on('data', chunk => {
            body += chunk.toString();
        });

        // All data received
        req.on('end', () => {
            console.log('Received:', body);
            res.end('Data received');
        });
    }
});
Compare to PHP: In PHP, POST data is instantly available in $_POST. In raw Node.js, you must handle the stream yourself. Express middleware does this for you.

Process Management: What Happens When It Crashes?

Unlike PHP running under Apache (where Apache handles process management), if your Node.js server crashes, it's dead. Nobody restarts it automatically.

# Start your server
$ node server.js
Server running on port 3000...

# If an unhandled exception occurs...
TypeError: Cannot read property 'x' of undefined
# Server exits. No more requests are handled.
# Users see "connection refused" until you manually restart.

This is another thing Apache handles for you: if a PHP script crashes, only that request fails. Apache keeps running and handles the next request normally.

Process Managers

In production, you need a process manager to:

PM2 is the most popular Node.js process manager:

# Install PM2 globally
$ npm install -g pm2

# Start your app with PM2
$ pm2 start server.js --name my-app

# PM2 keeps it running, restarts on crash
# View status
$ pm2 status
┌─────────┬────────┬──────────┬────────┬─────────┐
│ name    │ status │ restarts │ uptime │ memory  │
├─────────┼────────┼──────────┼────────┼─────────┤
│ my-app  │ online │ 0        │ 2h     │ 45.2mb  │
└─────────┴────────┴──────────┴────────┴─────────┘

# Make it start on system boot
$ pm2 startup
$ pm2 save
Another thing to manage: With PHP/Apache, the web server handles all of this. With Node.js, you must set up process management yourself. This is part of the DIY trade-off - more control, but more responsibility.

Why Learn This?

You'll probably use Express (next module), not raw http. But understanding this helps you:

Try It Live

We have a raw Node.js HTTP server (no Express!) running on this site. Visit it to see everything in action:

Live Demo: cse135.site:3001/demo

This demo shows you:

View the source: live-demo.js (~200 lines of raw Node.js)

Note: This demo runs on HTTP (not HTTPS) because raw Node.js doesn't include SSL/TLS handling. In production, you'd either add the https module with certificates or (more commonly) put nginx in front as a reverse proxy to handle SSL termination.

Summary