Node.js and Express course menu
Build an HTTP Server in Node.js (Without Express)
Create a web server with Node.js's built-in http module: handling requests and responses, status codes and headers, routing by method and URL, returning JSON, reading a POST body, testing with curl, and why frameworks like Express help.
On this page
A web server listens on a port, receives HTTP requests (a method such as GET or POST, a URL, headers, and sometimes a body) and sends back responses (a status code, headers and a body). Node's built-in node:http module can do all of this. Seeing it without a framework makes clear what Express does for you later.
A minimal server#
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end(`You asked for ${req.method} ${req.url}\n`);
});
server.listen(3000, () => console.log("Listening on http://localhost:3000"));node server.jsListening on http://localhost:3000The callback runs for every request, receiving req (the request: method, URL, headers, body stream) and res (the response: status, headers, body). res.end() sends the response; until it is called, the browser keeps waiting.
Open http://localhost:3000/hello in a browser, or use curl in a second terminal, a command-line HTTP client available on Windows 10+, macOS and Linux:
curl -s localhost:3000/hello
curl -s "localhost:3000/products?page=2"You asked for GET /hello
You asked for GET /products?page=2-s hides curl's progress bar. Stop the server with Ctrl+C.
Routing and JSON#
A real API answers differently depending on the method and path, and usually sends JSON:
import { createServer } from "node:http";
const products = [
{ id: 1, name: "Notebook", price: 60 },
{ id: 2, name: "Gel pen", price: 25 },
];
function sendJson(res, status, data) {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
const server = createServer((req, res) => {
const url = new URL(req.url, "http://localhost");
if (req.method === "GET" && url.pathname === "/api/products") {
return sendJson(res, 200, products);
}
const match = url.pathname.match(/^\/api\/products\/(\d+)$/);
if (req.method === "GET" && match) {
const product = products.find((p) => p.id === Number(match[1]));
return product ? sendJson(res, 200, product) : sendJson(res, 404, { error: "Product not found" });
}
sendJson(res, 404, { error: `Cannot ${req.method} ${url.pathname}` });
});
server.listen(3000);curl -s localhost:3000/api/products
curl -s localhost:3000/api/products/2
curl -s localhost:3000/api/products/9
curl -s localhost:3000/api/orders[{"id":1,"name":"Notebook","price":60},{"id":2,"name":"Gel pen","price":25}]
{"id":2,"name":"Gel pen","price":25}
{"error":"Product not found"}
{"error":"Cannot GET /api/orders"}Status codes tell the client what happened: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error. The Content-Type header says how to interpret the body.
Reading a POST body#
The request body arrives as a stream of chunks. Collect them, then parse the JSON:
import { createServer } from "node:http";
const products = [{ id: 1, name: "Notebook", price: 60 }];
async function readJson(req) {
let body = "";
for await (const chunk of req) body += chunk;
return JSON.parse(body);
}
createServer(async (req, res) => {
if (req.method === "POST" && req.url === "/api/products") {
try {
const data = await readJson(req);
if (!data.name || typeof data.price !== "number") {
res.writeHead(400, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: "name and numeric price are required" }));
}
const product = { id: products.length + 1, name: data.name, price: data.price };
products.push(product);
res.writeHead(201, { "Content-Type": "application/json" });
return res.end(JSON.stringify(product));
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: "Invalid JSON" }));
}
}
res.writeHead(404).end();
}).listen(3000);curl -s -X POST localhost:3000/api/products -H "Content-Type: application/json" -d '{"name":"Backpack","price":1299}'
curl -s -X POST localhost:3000/api/products -H "Content-Type: application/json" -d '{"name":"Lamp"}'
curl -s -X POST localhost:3000/api/products -H "Content-Type: application/json" -d 'not json'
curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/nothing{"id":2,"name":"Backpack","price":1299}
{"error":"name and numeric price are required"}
{"error":"Invalid JSON"}
404-X POST sets the method, -H adds a header, -d sends a body, and -w "%{http_code}" prints just the status code. (On Windows PowerShell, quoting JSON for curl is awkward; use Git Bash, WSL, or a tool such as Postman or the VS Code REST Client.)
Why use a framework?#
Even this small server needed manual URL parsing, a regex for the :id route, a helper for JSON, body collection, and repeated error responses. A real API also needs middleware (logging, authentication, CORS), validation, static files and centralised error handling. Express provides all of that with a small, readable API, which is why most Node.js back ends use it or a similar framework. The next lesson starts with Express.
Common mistakes#
Practice#
Practice exercise
Write a server where GET /time returns JSON { "hour": <number>, "greeting": "Good morning" | "Good afternoon" | "Good evening" } for an hour given as a query parameter (for example /time?hour=15), and responds with status 400 and an error message if hour is missing or not between 0 and 23.
Show hint
Read url.searchParams.get("hour"), convert with Number, and check Number.isInteger(hour) && hour >= 0 && hour <= 23.
Show solution
import { createServer } from "node:http";
createServer((req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname !== "/time") {
res.writeHead(404).end();
return;
}
const raw = url.searchParams.get("hour");
const hour = Number(raw);
res.setHeader("Content-Type", "application/json");
if (raw === null || !Number.isInteger(hour) || hour < 0 || hour > 23) {
res.writeHead(400).end(JSON.stringify({ error: "hour must be a whole number from 0 to 23" }));
return;
}
const greeting = hour < 12 ? "Good morning" : hour < 17 ? "Good afternoon" : "Good evening";
res.end(JSON.stringify({ hour, greeting }));
}).listen(3000);curl -s "localhost:3000/time?hour=9"
curl -s "localhost:3000/time?hour=19"
curl -s "localhost:3000/time?hour=25"{"hour":9,"greeting":"Good morning"}
{"hour":19,"greeting":"Good evening"}
{"error":"hour must be a whole number from 0 to 23"}Key takeaway#
Next: Getting Started with Express.
Finished this lesson?