Skip to content
Laviyx Learn

Try

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.

Intermediate9 min readUpdated
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#

server.js (JavaScript)
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"));
Terminal
node server.js
Output
Listening on http://localhost:3000

The 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:

Terminal
curl -s localhost:3000/hello
curl -s "localhost:3000/products?page=2"
Output
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:

api.js (JavaScript)
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);
Terminal
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
Output
[{"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:

post.js (JavaScript)
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);
Terminal
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
Output
{"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
time.js (JavaScript)
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);
Terminal
curl -s "localhost:3000/time?hour=9"
curl -s "localhost:3000/time?hour=19"
curl -s "localhost:3000/time?hour=25"
Output
{"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?