Node.js HTTP Server
What is the http Module?
Node.js's http module lets you build HTTP servers and clients without a framework like Express. Understanding the http module before learning Express gives you a much deeper understanding of how frameworks work internally.
Basic Server
const http = require('http');
const server = http.createServer((req, res) => {
// req: IncomingMessage (request object)
// res: ServerResponse (response object)
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>Hello, Node.js!</h1>');
});
server.listen(3000, 'localhost', () => {
console.log('Server started: http://localhost:3000');
});
// Error handling
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error('Port 3000 is already in use');
} else {
console.error('Server error:', err.message);
}
});
req (IncomingMessage) Object
Contains all information about the incoming request.
const http = require('http');
const server = http.createServer((req, res) => {
console.log('=== Request Info ===');
console.log('Method:', req.method); // GET, POST, PUT, DELETE
console.log('URL:', req.url); // /users?page=1
console.log('HTTP Version:', req.httpVersion); // 1.1
console.log('Headers:', req.headers);
// Commonly used headers
console.log('Content-Type:', req.headers['content-type']);
console.log('Authorization:', req.headers['authorization']);
console.log('User-Agent:', req.headers['user-agent']);
console.log('IP Address:', req.socket.remoteAddress);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
});
server.listen(3000);
Reading the Request Body
The body of POST/PUT requests is delivered as a stream:
const http = require('http');
function parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
// Block requests that are too large (security)
if (body.length > 1e6) {
req.destroy();
reject(new Error('Request size exceeded'));
}
});
req.on('end', () => {
try {
resolve(JSON.parse(body));
} catch {
resolve(body); // Return as string if not JSON
}
});
req.on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
if (req.method === 'POST') {
const body = await parseBody(req);
console.log('Request body:', body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: body }));
} else {
res.writeHead(405);
res.end('Method Not Allowed');
}
});
server.listen(3000);
res (ServerResponse) Object
Used to send a response to the client.
const http = require('http');
const server = http.createServer((req, res) => {
// Set status code + headers
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
'X-Custom-Header': 'my-value',
'Cache-Control': 'no-cache',
});
// Or set individually
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.setHeader('X-Request-Id', '12345');
// Read header
console.log(res.getHeader('Content-Type'));
// Remove header
res.removeHeader('X-Request-Id');
// Send response (header + body at once)
res.end(JSON.stringify({ message: 'success' }));
// Or write in parts
// res.write('first chunk');
// res.write('second chunk');
// res.end();
});
server.listen(3000);
URL Parsing and Manual Routing
const http = require('http');
const { URL } = require('url');
// Simple router implementation
const routes = new Map();
function get(path, handler) {
routes.set(`GET:${path}`, handler);
}
function post(path, handler) {
routes.set(`POST:${path}`, handler);
}
// Register routes
get('/', (req, res) => {
res.end(JSON.stringify({ message: 'Home page' }));
});
get('/users', (req, res, params, query) => {
const page = parseInt(query.get('page') || '1');
const limit = parseInt(query.get('limit') || '10');
res.end(JSON.stringify({ page, limit, users: [] }));
});
get('/users/:id', (req, res, params) => {
res.end(JSON.stringify({ id: params.id }));
});
post('/users', async (req, res) => {
let body = '';
for await (const chunk of req) {
body += chunk;
}
const user = JSON.parse(body);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ created: user }));
});
// Dynamic route matching function
function matchRoute(method, pathname) {
const key = `${method}:${pathname}`;
// Exact match
if (routes.has(key)) {
return { handler: routes.get(key), params: {} };
}
// Dynamic segment matching (:id etc.)
for (const [routeKey, handler] of routes) {
const [routeMethod, routePath] = routeKey.split(':');
if (routeMethod !== method) continue;
const routeSegments = routePath.split('/');
const pathSegments = pathname.split('/');
if (routeSegments.length !== pathSegments.length) continue;
const params = {};
const matched = routeSegments.every((seg, i) => {
if (seg.startsWith(':')) {
params[seg.slice(1)] = pathSegments[i];
return true;
}
return seg === pathSegments[i];
});
if (matched) return { handler, params };
}
return null;
}
const server = http.createServer(async (req, res) => {
const baseUrl = `http://${req.headers.host}`;
const url = new URL(req.url, baseUrl);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const match = matchRoute(req.method, url.pathname);
if (!match) {
res.writeHead(404);
res.end(JSON.stringify({ error: '404 Not Found' }));
return;
}
try {
await match.handler(req, res, match.params, url.searchParams);
} catch (err) {
res.writeHead(500);
res.end(JSON.stringify({ error: 'Server error', message: err.message }));
}
});
server.listen(3000, () => {
console.log('Router server: http://localhost:3000');
console.log('Test: curl http://localhost:3000/users?page=2&limit=5');
console.log('Test: curl http://localhost:3000/users/42');
});
Query String Parsing
const { URL, URLSearchParams } = require('url');
const querystring = require('querystring'); // old approach
// Recommended: use the URL class
const url = new URL('http://example.com/search?q=node.js&page=1&tags=js&tags=backend');
console.log(url.pathname); // /search
console.log(url.searchParams.get('q')); // node.js
console.log(url.searchParams.get('page')); // 1
console.log(url.searchParams.getAll('tags')); // ['js', 'backend']
console.log(url.searchParams.has('q')); // true
// Iterate
for (const [key, value] of url.searchParams) {
console.log(`${key}: ${value}`);
}
// URLSearchParams standalone
const params = new URLSearchParams('a=1&b=2&c=3');
params.set('d', '4');
params.delete('a');
console.log(params.toString()); // b=2&c=3&d=4
Serving Static Files
const http = require('http');
const fs = require('fs');
const path = require('path');
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff2': 'font/woff2',
};
const PUBLIC_DIR = path.join(__dirname, 'public');
async function serveStaticFile(req, res) {
// Prevent path traversal attacks (../ etc.)
const urlPath = decodeURIComponent(req.url.split('?')[0]);
const safePath = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
let filePath = path.join(PUBLIC_DIR, safePath);
// Serve index.html for directory access
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, 'index.html');
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
try {
const stat = await fs.promises.stat(filePath);
// ETag caching
const etag = `"${stat.mtime.getTime()}-${stat.size}"`;
if (req.headers['if-none-match'] === etag) {
res.writeHead(304);
res.end();
return;
}
res.writeHead(200, {
'Content-Type': contentType,
'Content-Length': stat.size,
'ETag': etag,
'Cache-Control': 'public, max-age=3600',
});
const stream = fs.createReadStream(filePath);
stream.pipe(res);
} catch {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
}
}
const server = http.createServer((req, res) => {
serveStaticFile(req, res);
});
server.listen(8080, () => {
console.log('Static file server: http://localhost:8080');
});
Understanding Middleware
Before Express, here is how the core idea of middleware can be implemented directly.
const http = require('http');
// Middleware: a function with the signature (req, res, next) => void
function logger(req, res, next) {
const start = Date.now();
const original = res.end.bind(res);
res.end = function (...args) {
const duration = Date.now() - start;
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
return original(...args);
};
next();
}
function cors(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
next();
}
function jsonBodyParser(req, res, next) {
if (req.headers['content-type']?.includes('application/json')) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
req.body = JSON.parse(body);
} catch {
req.body = null;
}
next();
});
} else {
next();
}
}
// Middleware chain runner
function applyMiddleware(middlewares, req, res, finalHandler) {
let index = 0;
function next() {
if (index < middlewares.length) {
middlewares[index++](req, res, next);
} else {
finalHandler(req, res);
}
}
next();
}
const middlewares = [logger, cors, jsonBodyParser];
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
applyMiddleware(middlewares, req, res, (req, res) => {
if (req.url === '/api/data' && req.method === 'POST') {
res.writeHead(200);
res.end(JSON.stringify({ echo: req.body }));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not Found' }));
}
});
});
server.listen(3000, () => {
console.log('Middleware server: http://localhost:3000');
});
Pro Tips
Graceful Shutdown
const http = require('http');
const server = http.createServer((req, res) => {
res.end('OK');
});
server.listen(3000);
// SIGTERM: process termination signal (PM2, Docker, etc.)
process.on('SIGTERM', () => {
console.log('Termination signal received, rejecting new connections...');
server.close(() => {
console.log('All connections closed, process exiting');
process.exit(0);
});
// Force exit timeout (30 seconds)
setTimeout(() => {
console.error('Forced exit');
process.exit(1);
}, 30_000);
});
keep-alive Connection Management
const http = require('http');
const server = http.createServer((req, res) => {
res.end('OK');
});
// Configure keep-alive timeout
server.keepAliveTimeout = 65_000; // 65 seconds
server.headersTimeout = 66_000; // 66 seconds (longer than keep-alive)
server.listen(3000);