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
- Request is a Readable stream (body comes in chunks)
- Response is a Writable stream (use
.write(),.end()) - Always call
res.end()or the request will hang - Pipe files directly for efficient static file serving
- Handle errors at request, server, and client levels
- Implement graceful shutdown for production servers