Creating a Basic HTTP Server with Node.js

Before Express existed, developers built HTTP servers with Node.js’s built-in http module. Express is a framework built directly on top of it. Understanding how the raw http module works โ€” how requests arrive, how you read the URL and method, how you write a response โ€” gives you a complete picture of what Express is actually doing on your behalf. In this lesson you will build a working HTTP server from scratch using only Node.js built-ins, see exactly why raw Node.js is cumbersome for an API, and appreciate the specific problems that Express solves.

What the http Module Provides

Capability http Module Express equivalent
Create a server http.createServer(callback) express()
Read request URL req.url (raw string, e.g. /api/posts?page=2) req.path, req.query.page
Read HTTP method req.method ('GET', 'POST'โ€ฆ) app.get(), app.post()โ€ฆ
Read request body Must manually collect chunks from a stream express.json() parses automatically
Send response res.writeHead(200); res.end(JSON.stringify(data)) res.status(200).json(data)
Route matching Manual if/else on req.url and req.method app.get('/api/posts', handler)
Middleware No concept โ€” must implement manually app.use(middleware)
Note: When you call app.listen(5000) in Express, Express calls http.createServer(app).listen(5000) internally. The Express app object itself is a valid request handler function โ€” it is designed to be passed directly to http.createServer(). This means Express is purely a JavaScript layer โ€” there is no C++ magic, no separate process โ€” just organised JavaScript on top of Node’s http module.
Tip: Building a raw HTTP server once is a great learning exercise, but do not use it for your real MERN API. Express exists because raw Node.js HTTP is verbose and error-prone at scale. After this lesson you will have a clear mental model of what Express does for you, which will help you debug problems and understand Express internals throughout the series.
Warning: Node.js request objects are readable streams. The request body (for POST/PUT requests) arrives in chunks and must be manually collected and concatenated before parsing as JSON. If you forget to handle the 'end' event you will never receive the body. Express’s express.json() middleware handles all of this for you โ€” another reason raw Node.js is not practical for REST APIs.

Building a Raw HTTP Server

// server-raw.js โ€” a complete HTTP server using only built-in Node.js modules
const http = require('http');
const url  = require('url');

// In-memory data store (no MongoDB yet)
let posts = [
  { id: 1, title: 'First Post',  body: 'Hello MERN!' },
  { id: 2, title: 'Second Post', body: 'Node.js is great' },
];

const server = http.createServer(async (req, res) => {
  // Parse the URL to separate the path from query string
  const parsed   = url.parse(req.url, true);
  const pathname = parsed.pathname;
  const method   = req.method;

  // Set CORS and content-type headers on every response
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Content-Type', 'application/json');

  // โ”€โ”€ Helper: read request body โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function readBody() {
    return new Promise((resolve, reject) => {
      let body = '';
      req.on('data',  chunk => body += chunk.toString());
      req.on('end',   ()    => resolve(body));
      req.on('error', err   => reject(err));
    });
  }

  // โ”€โ”€ Route: GET /api/health โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (pathname === '/api/health' && method === 'GET') {
    res.writeHead(200);
    res.end(JSON.stringify({ status: 'ok' }));
    return;
  }

  // โ”€โ”€ Route: GET /api/posts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (pathname === '/api/posts' && method === 'GET') {
    res.writeHead(200);
    res.end(JSON.stringify({ success: true, data: posts }));
    return;
  }

  // โ”€โ”€ Route: POST /api/posts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (pathname === '/api/posts' && method === 'POST') {
    const raw  = await readBody();
    const body = JSON.parse(raw);
    const post = { id: posts.length + 1, ...body };
    posts.push(post);
    res.writeHead(201);
    res.end(JSON.stringify({ success: true, data: post }));
    return;
  }

  // โ”€โ”€ 404 fallthrough โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  res.writeHead(404);
  res.end(JSON.stringify({ success: false, message: 'Route not found' }));
});

server.listen(5000, () => console.log('Raw HTTP server on http://localhost:5000'));

The Same API in Express โ€” Side by Side

// server-express.js โ€” the same API with Express
const express = require('express');
const cors    = require('cors');
const app     = express();

let posts = [
  { id: 1, title: 'First Post',  body: 'Hello MERN!' },
  { id: 2, title: 'Second Post', body: 'Node.js is great' },
];

app.use(cors());          // CORS handled in one line
app.use(express.json());  // body parsing handled in one line

// Routes are clean, declarative, and easy to read
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));

app.get('/api/posts', (req, res) => {
  res.json({ success: true, data: posts });
});

app.post('/api/posts', (req, res) => {
  const post = { id: posts.length + 1, ...req.body }; // body already parsed!
  posts.push(post);
  res.status(201).json({ success: true, data: post });
});

// 404 fallthrough
app.use((req, res) => res.status(404).json({ message: 'Route not found' }));

app.listen(5000, () => console.log('Express server on http://localhost:5000'));

What Express Removes

Raw Node.js http Lines of code Express equivalent Lines of code
Manual URL parsing 3โ€“4 Built into route definition 0
Manual body collection 7โ€“10 app.use(express.json()) 1
Manual CORS headers 3โ€“5 app.use(cors()) 1
if/else route matching 4 per route app.get('/path', handler) 1
res.writeHead + res.end 2 per response res.json(data) 1

Common Mistakes

Mistake 1 โ€” Not ending the response

โŒ Wrong โ€” forgetting to call res.end() or res.json() hangs the request forever:

// Raw http
if (pathname === '/api/posts') {
  res.writeHead(200);
  // forgot res.end() โ€” client waits forever, then times out
}

// Express
app.get('/api/posts', (req, res) => {
  const posts = getPosts();
  // forgot res.json(posts) โ€” same problem
});

โœ… Correct โ€” every code path through a route handler must call a response method.

Mistake 2 โ€” Not parsing the body in raw http

โŒ Wrong โ€” reading req.body in raw Node.js http (it does not exist):

const server = http.createServer((req, res) => {
  const data = req.body; // undefined โ€” body is a stream, not a property
  const post = JSON.parse(data); // TypeError: cannot parse undefined
});

โœ… Correct โ€” collect the stream manually (or just use Express which does this for you).

Mistake 3 โ€” Confusing http.createServer with app.listen

โŒ Wrong assumption โ€” thinking app.listen() is entirely separate from Node’s http module:

// Common misconception: Express.listen() is something special
// Reality:
app.listen(5000) === http.createServer(app).listen(5000)
// Express's app object IS the request handler passed to http.createServer()

โœ… Understanding this matters when you add Socket.io โ€” you need to pass the http server instance to Socket.io, not the Express app, which requires creating the http server explicitly.

Quick Reference

Task Raw http Express
Create server http.createServer(fn) express()
Read URL url.parse(req.url) req.path, req.query
Read method req.method app.get/post/put/delete
Send JSON res.writeHead(200); res.end(JSON.stringify(d)) res.json(d)
Send status res.writeHead(404) res.status(404)
Start listening server.listen(PORT) app.listen(PORT)

🧠 Test Yourself

When using the raw Node.js http module to handle a POST request, why can you not read the request body with req.body like you can in Express?