HTTP Server Fundamentals

Node's HTTP module lets you build web servers from scratch. Understanding the underlying mechanics helps you appreciate what frameworks like Express abstract away.

Creating a Basic Server

const http = require('http');

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

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

The Request Object

The request (http.IncomingMessage) is a Readable Stream:

http.createServer((req, res) => {
  // Request metadata
  console.log(req.method);       // 'GET', 'POST', etc.
  console.log(req.url);          // '/path?query=string'
  console.log(req.headers);      // { 'content-type': '...', ... }
  console.log(req.httpVersion);  // '1.1'

  // For POST/PUT - read body as stream
  let body = '';
  req.on('data', (chunk) => {
    body += chunk.toString();
  });
  req.on('end', () => {
    console.log('Body:', body);
  });
});

Parsing URL

const url = require('url');

http.createServer((req, res) => {
  const parsedUrl = new URL(req.url, `http://${req.headers.host}`);

  console.log(parsedUrl.pathname);  // '/users'
  console.log(parsedUrl.search);    // '?id=123'
  console.log(parsedUrl.searchParams.get('id')); // '123'
});

The Response Object

The response (http.ServerResponse) is a Writable Stream:

http.createServer((req, res) => {
  // Set status code
  res.statusCode = 200;

  // Set headers
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('X-Custom-Header', 'value');

  // Or all at once
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  });

  // Write body
  res.write('{"message": ');
  res.write('"Hello"}');

  // End response (required!)
  res.end();

  // Or write and end together
  res.end('{"message": "Hello"}');
});

Routing Without Frameworks

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

http.createServer((req, res) => {
  const { method, url } = req;

  if (method === 'GET' && url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    fs.createReadStream('index.html').pipe(res);
  }
  else if (method === 'GET' && url === '/api/users') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify([{ id: 1, name: 'Alice' }]));
  }
  else if (method === 'POST' && url === '/api/users') {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      const user = JSON.parse(body);
      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ id: 2, ...user }));
    });
  }
  else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
}).listen(3000);

Serving Static Files

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

const MIME_TYPES = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
};

http.createServer((req, res) => {
  if (req.method !== 'GET') {
    res.writeHead(405);
    return res.end('Method Not Allowed');
  }

  let filePath = path.join(__dirname, 'public', req.url);

  // Security: prevent directory traversal
  if (!filePath.startsWith(path.join(__dirname, 'public'))) {
    res.writeHead(403);
    return res.end('Forbidden');
  }

  const ext = path.extname(filePath);
  const contentType = MIME_TYPES[ext] || 'application/octet-stream';

  fs.access(filePath, fs.constants.R_OK, (err) => {
    if (err) {
      res.writeHead(404);
      return res.end('Not Found');
    }

    res.writeHead(200, { 'Content-Type': contentType });
    fs.createReadStream(filePath).pipe(res);
  });
}).listen(3000);

Handling POST Data

function parseBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];

    req.on('data', (chunk) => chunks.push(chunk));

    req.on('end', () => {
      const body = Buffer.concat(chunks).toString();
      const contentType = req.headers['content-type'];

      try {
        if (contentType === 'application/json') {
          resolve(JSON.parse(body));
        } else if (contentType === 'application/x-www-form-urlencoded') {
          resolve(Object.fromEntries(new URLSearchParams(body)));
        } else {
          resolve(body);
        }
      } catch (err) {
        reject(err);
      }
    });

    req.on('error', reject);
  });
}

// Usage
http.createServer(async (req, res) => {
  if (req.method === 'POST') {
    try {
      const data = await parseBody(req);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ received: data }));
    } catch (err) {
      res.writeHead(400);
      res.end('Bad Request');
    }
  }
}).listen(3000);

Connection Keep-Alive

HTTP/1.1 connections are persistent by default:

const server = http.createServer((req, res) => {
  res.end('Hello');
});

// Control keep-alive
server.keepAliveTimeout = 5000;  // 5 seconds
server.headersTimeout = 60000;   // 60 seconds for headers

server.on('connection', (socket) => {
  console.log('New TCP connection');
  socket.on('close', () => {
    console.log('Connection closed');
  });
});

Error Handling

const server = http.createServer((req, res) => {
  try {
    // Handle request
    handleRequest(req, res);
  } catch (err) {
    console.error('Request error:', err);
    res.writeHead(500);
    res.end('Internal Server Error');
  }
});

// Handle server-level errors
server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error('Port already in use');
  } else {
    console.error('Server error:', err);
  }
});

// Handle client errors
server.on('clientError', (err, socket) => {
  if (err.code === 'ECONNRESET' || !socket.writable) {
    return;
  }
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

Server Events

const server = http.createServer(handler);

server.on('request', (req, res) => {
  // Same as callback to createServer
});

server.on('connection', (socket) => {
  // New TCP connection
});

server.on('upgrade', (req, socket, head) => {
  // WebSocket upgrade request
});

server.on('close', () => {
  // Server stopped
});

server.on('error', (err) => {
  // Server error
});

Graceful Shutdown

const server = http.createServer(handler);
const connections = new Set();

server.on('connection', (socket) => {
  connections.add(socket);
  socket.on('close', () => connections.delete(socket));
});

function shutdown() {
  console.log('Shutting down...');

  server.close(() => {
    console.log('Server closed');
    process.exit(0);
  });

  // Close existing connections
  for (const socket of connections) {
    socket.destroy();
  }

  // Force exit after timeout
  setTimeout(() => {
    console.log('Forcing exit');
    process.exit(1);
  }, 10000);
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

Key Takeaways

  1. Request is a Readable stream (body comes in chunks)
  2. Response is a Writable stream (use .write(), .end())
  3. Always call res.end() or the request will hang
  4. Pipe files directly for efficient static file serving
  5. Handle errors at request, server, and client levels
  6. Implement graceful shutdown for production servers