HTTP Server

Building a web server from scratch using Node's built-in http module

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!"

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}`); });

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('

Home Page

'); } else if (req.url === '/about' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('

About Page

'); } 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('

404 Not Found

'); } }); server.listen(3000);

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!

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'); }); } });

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:

  • Automatically restart the server if it crashes
  • Start the server on system boot
  • Monitor memory usage and restart if it gets too high
  • Provide logging and status monitoring

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

Why Learn This?

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

  • Appreciate what Express abstracts away
  • Debug issues when things go wrong
  • Understand Node.js's streaming/event-based nature
  • Write custom middleware or servers when needed

Summary

  • Node.js creates its own HTTP server with http.createServer()
  • The server runs continuously until you stop it
  • req contains request info: URL, method, headers (accessed by lowercase key)
  • Query strings require manual parsing with the url module
  • res is used to send the response with status, headers, and body
  • Manual routing, MIME types, and file serving is tedious — you're essentially rebuilding what Apache does
  • This DIY approach has trade-offs: flexibility and minimal surface area vs. missing features you may forget to implement