Skip to main content
Advertisement

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);
Advertisement