
How to Create a Web Server in Node.js with the HTTP Module
Building a web server in Node.js using the HTTP module is a fundamental skill every backend developer should master. Unlike higher-level frameworks like Express or Koa, the raw HTTP module gives you complete control over how your server handles requests and responses. You’ll learn how to create production-ready servers from scratch, handle different HTTP methods, manage routing, implement middleware-like functionality, and troubleshoot common issues that arise when working directly with Node’s core HTTP capabilities.
How the Node.js HTTP Module Works
The HTTP module in Node.js is built on top of the underlying network layer and provides an abstraction for creating HTTP servers and clients. When you create a server using http.createServer()
, Node establishes a TCP connection that listens for incoming HTTP requests on a specified port.
The server operates on an event-driven architecture where each incoming request triggers a callback function with two main objects: the request object (IncomingMessage) containing all client data, and the response object (ServerResponse) for sending data back to the client. The request object provides access to headers, URL parameters, HTTP method, and request body, while the response object lets you set status codes, headers, and send the actual response data.
Here’s the basic server creation pattern:
const http = require('http');
const server = http.createServer((req, res) => {
// Handle request and send response
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Step-by-Step Server Implementation Guide
Step 1: Basic Server Setup
Start by creating a minimal HTTP server that responds to all requests:
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const method = req.method.toLowerCase();
console.log(`${method.toUpperCase()} ${path}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'Server is running',
path: path,
method: method,
timestamp: new Date().toISOString()
}));
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
Step 2: Implementing Route Handling
Create a simple routing system to handle different endpoints:
const http = require('http');
const url = require('url');
const routes = {
'/': (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Welcome to Node.js Server</h1>');
},
'/api/users': (req, res) => {
const users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
},
'/api/status': (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
uptime: process.uptime(),
memory: process.memoryUsage()
}));
}
};
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
if (routes[path]) {
routes[path](req, res);
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Route not found' }));
}
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Step 3: Handling Different HTTP Methods
Extend your server to handle POST, PUT, and DELETE requests with body parsing:
const http = require('http');
const url = require('url');
// Helper function to parse request body
function parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch (error) {
reject(error);
}
});
req.on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const method = req.method.toLowerCase();
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// Handle preflight requests
if (method === 'options') {
res.writeHead(200);
res.end();
return;
}
try {
if (path === '/api/data' && method === 'get') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'GET request received' }));
} else if (path === '/api/data' && method === 'post') {
const body = await parseBody(req);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'POST request received',
data: body
}));
} else if (path === '/api/data' && method === 'put') {
const body = await parseBody(req);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'PUT request received',
updated: body
}));
} else if (path === '/api/data' && method === 'delete') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'DELETE request received' }));
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Endpoint not found' }));
}
} catch (error) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
}
});
server.listen(3000, () => {
console.log('Server with full HTTP method support running on port 3000');
});
Real-World Examples and Use Cases
File Server Implementation
Here’s a practical example of serving static files with proper MIME types:
const http = require('http');
const fs = require('fs');
const path = require('path');
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm'
};
const server = http.createServer((req, res) => {
let filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url);
// Security check to prevent directory traversal
if (!filePath.startsWith(path.join(__dirname, 'public'))) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Access Denied');
return;
}
const extname = String(path.extname(filePath)).toLowerCase();
const contentType = mimeTypes[extname] || 'application/octet-stream';
fs.readFile(filePath, (error, content) => {
if (error) {
if (error.code === 'ENOENT') {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>404 - File Not Found</h1>');
} else {
res.writeHead(500);
res.end('Server Error: ' + error.code);
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});
server.listen(8080, () => {
console.log('File server running on http://localhost:8080');
});
RESTful API with In-Memory Database
const http = require('http');
const url = require('url');
// Simple in-memory data store
let users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
let nextId = 3;
function parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch (error) {
reject(error);
}
});
});
}
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const method = req.method.toLowerCase();
const pathParts = path.split('/').filter(part => part);
res.setHeader('Content-Type', 'application/json');
try {
// GET /api/users - Get all users
if (path === '/api/users' && method === 'get') {
res.writeHead(200);
res.end(JSON.stringify(users));
// GET /api/users/:id - Get user by ID
} else if (pathParts[0] === 'api' && pathParts[1] === 'users' && pathParts[2] && method === 'get') {
const userId = parseInt(pathParts[2]);
const user = users.find(u => u.id === userId);
if (user) {
res.writeHead(200);
res.end(JSON.stringify(user));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'User not found' }));
}
// POST /api/users - Create new user
} else if (path === '/api/users' && method === 'post') {
const body = await parseBody(req);
if (!body.name || !body.email) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Name and email are required' }));
return;
}
const newUser = {
id: nextId++,
name: body.name,
email: body.email
};
users.push(newUser);
res.writeHead(201);
res.end(JSON.stringify(newUser));
// PUT /api/users/:id - Update user
} else if (pathParts[0] === 'api' && pathParts[1] === 'users' && pathParts[2] && method === 'put') {
const userId = parseInt(pathParts[2]);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
res.writeHead(404);
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
const body = await parseBody(req);
users[userIndex] = { ...users[userIndex], ...body };
res.writeHead(200);
res.end(JSON.stringify(users[userIndex]));
// DELETE /api/users/:id - Delete user
} else if (pathParts[0] === 'api' && pathParts[1] === 'users' && pathParts[2] && method === 'delete') {
const userId = parseInt(pathParts[2]);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
res.writeHead(404);
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
users.splice(userIndex, 1);
res.writeHead(204);
res.end();
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Route not found' }));
}
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ error: 'Internal server error' }));
}
});
server.listen(3000, () => {
console.log('RESTful API server running on http://localhost:3000');
});
Comparison with Alternatives
Feature | Raw HTTP Module | Express.js | Fastify | Koa.js |
---|---|---|---|---|
Bundle Size | Built-in (0 KB) | ~209 KB | ~167 KB | ~46 KB |
Learning Curve | Steep | Moderate | Moderate | Moderate-Steep |
Performance | Highest | Good | Excellent | Good |
Built-in Middleware | None | Extensive | Good | Minimal |
Routing | Manual | Built-in | Built-in | Router package |
JSON Parsing | Manual | Built-in | Built-in | Built-in |
Error Handling | Manual | Built-in | Built-in | Built-in |
Performance Benchmarks
Based on typical benchmarking scenarios using autocannon
with 10 connections over 30 seconds:
Framework | Requests/sec | Latency (avg) | Memory Usage |
---|---|---|---|
Raw HTTP | ~45,000 | 2.1ms | 25 MB |
Express.js | ~38,000 | 2.6ms | 32 MB |
Fastify | ~42,000 | 2.3ms | 28 MB |
Koa.js | ~40,000 | 2.4ms | 30 MB |
Best Practices and Common Pitfalls
Essential Best Practices
- Always handle errors properly: Wrap async operations in try-catch blocks and implement proper error responses
- Set appropriate HTTP status codes: Use 200 for success, 201 for creation, 400 for client errors, 500 for server errors
- Implement request timeouts: Prevent hanging connections by setting server timeouts
- Validate input data: Never trust client input without proper validation and sanitization
- Use environment variables: Store configuration like ports and API keys in environment variables
- Implement proper CORS headers: Configure Cross-Origin Resource Sharing for web applications
Here’s a production-ready server template with security and error handling:
const http = require('http');
const url = require('url');
class NodeServer {
constructor(options = {}) {
this.port = options.port || process.env.PORT || 3000;
this.timeout = options.timeout || 30000;
this.routes = new Map();
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
route(method, path, handler) {
const key = `${method.toUpperCase()}:${path}`;
this.routes.set(key, handler);
}
async handleRequest(req, res) {
// Set security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
const startTime = Date.now();
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const method = req.method.toUpperCase();
const routeKey = `${method}:${path}`;
try {
// Run middlewares
for (const middleware of this.middlewares) {
await middleware(req, res);
}
// Find and execute route handler
if (this.routes.has(routeKey)) {
await this.routes.get(routeKey)(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Route not found',
path: path,
method: method
}));
}
} catch (error) {
console.error(`Error handling ${method} ${path}:`, error);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Internal server error',
timestamp: new Date().toISOString()
}));
}
} finally {
const responseTime = Date.now() - startTime;
console.log(`${method} ${path} - ${res.statusCode} - ${responseTime}ms`);
}
}
start() {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
this.server.timeout = this.timeout;
this.server.on('error', (error) => {
console.error('Server error:', error);
});
this.server.listen(this.port, () => {
console.log(`Server running on http://localhost:${this.port}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
this.server.close(() => {
console.log('Server closed.');
process.exit(0);
});
});
}
}
// Usage example
const app = new NodeServer({ port: 3000, timeout: 30000 });
// Logging middleware
app.use(async (req, res) => {
req.timestamp = new Date().toISOString();
});
// JSON body parser middleware
app.use(async (req, res) => {
if (req.method === 'POST' || req.method === 'PUT') {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
try {
req.body = body ? JSON.parse(body) : {};
resolve();
} catch (error) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
reject(error);
}
});
});
}
});
// Define routes
app.route('GET', '/', async (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'Server is running',
timestamp: req.timestamp,
uptime: process.uptime()
}));
});
app.route('POST', '/api/data', async (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
received: req.body,
timestamp: req.timestamp
}));
});
app.start();
Common Pitfalls to Avoid
- Not handling the request body properly: Always check if the request has a body before parsing it
- Forgetting to call res.end(): This will leave connections hanging and eventually cause timeouts
- Setting headers after sending response: Headers must be set before calling res.write() or res.end()
- Not validating content-type: Check the Content-Type header before parsing JSON bodies
- Ignoring URL encoding: Use decodeURIComponent() for URL parameters with special characters
- Not implementing proper error boundaries: Unhandled exceptions can crash your entire server
Security Considerations
- Input validation: Sanitize all user input to prevent injection attacks
- Rate limiting: Implement request rate limiting to prevent abuse
- HTTPS in production: Always use HTTPS in production environments
- Security headers: Set appropriate security headers like CSP, HSTS, and X-Frame-Options
- File upload limits: Set maximum payload sizes to prevent memory exhaustion
For production deployments, consider using a VPS hosting solution that provides the flexibility to configure your Node.js server exactly as needed, or opt for dedicated servers for high-traffic applications requiring maximum performance and control.
Performance Optimization Tips
- Use clustering: Utilize Node’s cluster module to spawn multiple worker processes
- Implement caching: Cache frequently accessed data in memory or using Redis
- Compress responses: Use gzip compression for text-based responses
- Optimize JSON operations: Use streaming JSON parsers for large payloads
- Monitor memory usage: Implement memory usage monitoring and cleanup routines
The HTTP module documentation is available at the official Node.js documentation, which provides comprehensive details about all available methods and properties. For additional learning resources, the MDN HTTP documentation offers excellent background on HTTP fundamentals that complement Node.js server development.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.