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.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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();
});
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#
- new URL(req.url, base) splits the path from the query string.
- Routing is a chain of if statements on method and path.
- readJson collects the body chunks with for await and enforces a size limit.
- sendJson sets the status and Content-Type and serialises the data.
- 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#
Without ending the response, the client waits until it times out.
Reading unlimited bodies lets anyone exhaust your memory. Enforce a limit.
/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?
Show solution
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#
Which call finishes an HTTP response in the http module?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.