Chapter 11 of 18 · Node.js for Beginners — Complete Free Tutorial

Building a Server with the http Module

HTTP is just text, so Node can speak it with a single built-in module. The Node.js for Beginners series now builds a real server from scratch: http.createServer, routing by method and path, JSON responses, and how to read a request body without blocking the loop. This is the machinery Express hides in chapter 12 — understanding it makes Express obvious.

Your first server

import http from 'node:http';

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Hello from Node!');
});

server.listen(3000, () => {
  console.log('listening on http://localhost:3000');
});
node server.js
curl -s http://localhost:3000

Expected output:

listening on http://localhost:3000
Hello from Node!

Three things are worth noticing. listen starts a long-running process — the script does not exit, because the event loop now has a listening socket (exactly the "never exits" behaviour chapter 3 hinted at). The callback runs only once the port is actually bound. And the same file works with require('node:http') if your project is CommonJS — only the import line changes.

Reading the request

The req argument is an incoming message: method, URL, headers and a stream of body bytes. The res argument is where you write the reply.

const server = http.createServer((req, res) => {
  console.log(req.method, req.url, req.headers['user-agent'] ?? 'unknown');

  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
  } else {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'not found' }));
  }
});

Expected output: a log line such as GET /health curl/8.7.1, and {"status":"ok"} at /health — with {"error":"not found"} plus a genuine 404 status everywhere else. This if/else ladder is routing; Express packages it as app.get('/health', handler).

A small JSON helper

Every API route repeats the same three lines — set status, set content type, serialise — so factor them once:

function sendJSON(res, status, payload) {
  const body = JSON.stringify(payload);
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': Buffer.byteLength(body),
  });
  res.end(body);
}

// usage inside the request handler
sendJSON(res, 200, { todos: [], count: 0 });

Expected output: {"todos":[],"count":0}. Setting Content-Length explicitly is optional in modern Node (it computes it for you) but a useful habit when bodies are streamed — and it keeps HTTP/1.1 clients from waiting on a timeout.

Reading a request body

A body arrives in chunks on a stream; collect them, then parse. Because parsing can throw, guard it and answer 400 — never crash the process on bad client input.

if (req.method === 'POST' && req.url === '/api/todos') {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);

  let todo;
  try {
    todo = JSON.parse(Buffer.concat(chunks).toString('utf8'));
  } catch {
    return sendJSON(res, 400, { error: 'invalid JSON' });
  }
  if (!todo?.title || typeof todo.title !== 'string') {
    return sendJSON(res, 422, { error: 'title is required' });
  }

  sendJSON(res, 201, { id: 1, title: todo.title, done: false });
}

Expected output: a valid POST replies {"id":1,"title":"...","done":false} with status 201; a malformed body replies {"error":"invalid JSON"} with 400. The for await loop is the same stream machinery you met in chapter 7 — top-level await inside a handler needs an async wrapper, which is one of several reasons Express's body parser is worth having.

Mapping methods to actions

RequestSemanticsTypical response
GET /api/todosList todos200 + array
POST /api/todosCreate one201 + the new todo
PATCH /api/todos/1Update one200 + updated, or 404
DELETE /api/todos/1Delete one204 no body, or 404
Anything unknownNo route404 + error JSON

Extracting the id from /api/todos/1 means string splitting, query strings mean URLSearchParams, and error handling means nested try/catch — three problems Express solved long ago, which is where this series goes next.

Common mistakes

  • Never calling res.end() — the request hangs until the client times out; every path must end the response.
  • EADDRINUSE — port 3000 is taken by a leftover process; kill it or pick another port.
  • Crashing on bad input — JSON.parse throws; uncaught exceptions take the whole server down (chapter 9).
  • Forgetting Content-Type — clients guess or refuse to parse the body.
  • Manual path matching without normalising — /todos/ and /todos are different strings; decide a canonical form early.

Practice checklist

  1. Start the server and hit it with curl -i; confirm your status line and headers.
  2. Add a GET /about route returning JSON and verify the 404 fallback still works.
  3. POST a valid and an invalid JSON body; confirm 201 versus 400 responses.
  4. Log every request's method, URL and duration to the console as a mini access log.
  5. Reproduce EADDRINUSE, then fix it two ways: kill the process and change the port.

The raw module works — now let a battle-tested framework carry the plumbing. Next: Getting Started with Express.

Quiz: Building a Server with the http Module

5 questions · 70% to pass

Frequently Asked Questions

Node.js is a runtime that executes JavaScript outside the browser. It combines Google's V8 engine with APIs for files, networks, processes and streams, plus a package ecosystem (npm). You use it to build servers, command-line tools, APIs and anything that needs JavaScript running on a machine rather than in a page.

JavaScript is the language; Node.js is one environment that runs it. In the browser the language comes with a DOM, events and window APIs — in Node those are replaced by the file system, network and process modules. Syntax, functions, arrays, promises and async/await are identical, so your JavaScript knowledge transfers directly.

Browser code is sandboxed: it cannot read your files or open arbitrary ports. Node code has full machine access, which is why it is used for servers and tools but also why you never run untrusted code in it. There is no document or window in Node — the equivalents are global, process and modules.

npm is JavaScript's package manager and public registry. package.json describes your project: name, entry point, dependencies with version ranges, and scripts such as npm run dev. Installing a package copies it into node_modules and records the exact intent in package-lock.json so every machine installs identical versions.

Express is a minimal web framework for Node: a few thousand lines that give you routing, middleware, request parsing and static files on top of the raw HTTP server. It is the most common starting point for REST APIs and the backbone of this tutorial's project chapters.

Both work. CommonJS (require/module.exports) is the classic Node format; ES modules (import/export) are the standard and activated in a package.json with "type": "module". Modern projects favour ESM for browser compatibility, but you will read both in existing code, so this course teaches each.

Middleware are functions that run between the incoming request and the final response, in order: body parsing, logging, authentication, rate limits, error handling. Each receives the request, response and next function and can respond itself or pass control along — Express's pipeline model in one sentence.

A JSON file is perfect for learning CRUD: no setup, readable data, easy to reset. The moment you need concurrent writes, querying or multiple users, move to a real database — MySQL/PostgreSQL for relational data or MongoDB for documents. The storage chapter isolates persistence behind one module so swapping it later is straightforward.

A REST API exposes resources over HTTP using verbs on URLs: GET /api/todos lists, POST creates, PUT or PATCH updates, DELETE removes, and everything answers with JSON plus correct status codes (200, 201, 400, 404, 500). It is stateless — each request carries its own authentication — which makes it easy to cache, scale and consume from any frontend.

Node apps are long-running processes, not static files: deploy them to a Node-friendly host (a VPS, Render, Railway or Fly.io), keep secrets in environment variables, terminate HTTPS at a reverse proxy or platform edge, and send logs somewhere durable. The final chapter covers the full checklist including health checks and graceful shutdown.