BLOG POSTS
    MangoHost Blog / How to Create a Web Server in Node.js with the HTTP Module
How to Create a Web Server in Node.js with the HTTP Module

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.

Leave a reply

Your email address will not be published. Required fields are marked