Node.js · Web Servers

HTTP Module

Every Node web framework — Express, Fastify, NestJS — is built on the http module. Writing a small server with it first shows you what frameworks do for you.

Intermediate 6 min Lesson 18 of 37Examples target Node.js 22 LTS.
Before you start:Async and AwaitStreams
0/37 in Node.js
On this page · 6 min · Intermediate

Learning objectives#

  • Create a server with http.createServer
  • Route by method and URL
  • Send JSON with the right status and headers
  • Read and parse a JSON request body

Explanation#

http.createServer((req, res) => {...}) calls your function for every request. req has method, url and headers; res lets you set the status, headers and body.

Routing is manual: check req.method and the path (new URL(req.url, "http://localhost") parses the path and query string).

res.writeHead(status, headers) then res.end(body). For JSON, set Content-Type: application/json and send JSON.stringify(data).

The request body arrives as a stream of chunks. Collect them (for await of req), concatenate, and JSON.parse — with try/catch and a size limit.

This is a lot of plumbing — which is exactly why Express exists. Use the http module to understand the basics, and a framework for real APIs.

Syntax#

JavaScript Illustrative
http.createServer(async (req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ ok: true }));
  }
  res.writeHead(404).end();
}).listen(3000);

Code example: A tiny JSON API without any framework#

JavaScriptserver.js Illustrative
import http from 'node:http';


const products = [{ id: 1, name: 'Pen', price: 20 }];


function sendJson(res, status, data) {
  res.writeHead(status, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}


async function readJson(req, limit = 10_000) {
  let body = '';
  for await (const chunk of req) {
    body += chunk;
    if (body.length > limit) throw new Error('Body too large');
  }
  return JSON.parse(body || '{}');
}


const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost');


  if (req.method === 'GET' && url.pathname === '/api/products') {
    const max = Number(url.searchParams.get('maxPrice') ?? Infinity);
    return sendJson(res, 200, products.filter(p => p.price <= max));
  }


  if (req.method === 'POST' && url.pathname === '/api/products') {
    try {
      const { name, price } = await readJson(req);
      if (!name || !(price > 0)) return sendJson(res, 400, { error: 'name and a positive price are required' });
      const product = { id: products.length + 1, name, price };
      products.push(product);
      return sendJson(res, 201, product);
    } catch {
      return sendJson(res, 400, { error: 'Invalid JSON' });
    }
  }


  sendJson(res, 404, { error: `No route for ${req.method} ${url.pathname}` });
});


server.listen(3002, async () => {
  const base = 'http://localhost:3002/api/products';
  const show = async r => console.log(r.status, await r.text());


  await show(await fetch(base, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Bag', price: 650 }) }));
  await show(await fetch(base + '?maxPrice=100'));
  await show(await fetch(base, { method: 'POST', body: '{bad json' }));
  await show(await fetch('http://localhost:3002/nope'));
  server.close();
});
Expected output
201 {"id":2,"name":"Bag","price":650}
200 [{"id":1,"name":"Pen","price":20}]
400 {"error":"Invalid JSON"}
404 {"error":"No route for GET /nope"}

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. new URL(req.url, base) splits the path from the query string.
  2. Routing is a chain of if statements on method and path.
  3. readJson collects the body chunks with for await and enforces a size limit.
  4. sendJson sets the status and Content-Type and serialises the data.
  5. Unknown routes fall through to a 404 response.

Real-world use case#

A tiny internal webhook receiver or health-check service with no dependencies at all — or simply understanding what Express does under the hood when debugging it.

Common mistakes#

Forgetting res.end()

Without ending the response, the client waits until it times out.

No body size limit

Reading unlimited bodies lets anyone exhaust your memory. Enforce a limit.

Comparing req.url with a query string

/api/products?maxPrice=100 does not equal "/api/products". Parse the URL first.

Best practices#

  • Parse URLs with new URL()
  • Always set Content-Type
  • Limit and validate request bodies
  • Use a framework for anything non-trivial

Remember this#

createServer((req, res) => …) runs per request.

writeHead(status, headers) + end(body).

Body = stream → collect → parse → validate.

Practice exercise#

Your task: Add a GET /api/products/:id route to the server: parse the id from the path and return 404 if the product does not exist.

Need a hint?
Match url.pathname with /^\/api\/products\/(\d+)$/.
Show solution
JavaScript Illustrative
import http from 'node:http';


const products = [{ id: 1, name: 'Pen', price: 20 }];


const server = http.createServer((req, res) => {
  const url = new URL(req.url, 'http://localhost');
  const match = url.pathname.match(/^\/api\/products\/(\d+)$/);
  if (req.method === 'GET' && match) {
    const product = products.find(p => p.id === Number(match[1]));
    res.writeHead(product ? 200 : 404, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(product ?? { error: 'Not found' }));
  }
  res.writeHead(404).end();
});


server.listen(3003, async () => {
  console.log(await (await fetch('http://localhost:3003/api/products/1')).text());   // {"id":1,...}
  console.log((await fetch('http://localhost:3003/api/products/9')).status);         // 404
  server.close();
});

Mini quiz#

HTTP Module quizMultiple choice Question 1 of 3

Which call finishes an HTTP response in the http module?

Score so far: 0/0

Want more? Practise all Node.js MCQs · Node.js interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.