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

Building a REST API

This chapter starts the project that runs to the end of the Node.js for Beginners series: a to-do REST API. You already have Express, routes, parameters and middleware — now they become a coherent resource with correct verbs, correct status codes and validation. The data lives in memory for now; chapter 16 makes it survive restarts, chapter 17 gives it a frontend, and chapter 18 makes it production-ready.

The contract

Design the API before writing it. One resource, one base path, five operations:

Method and pathActionSuccessFailure
GET /api/todosList all todos200 + array—
GET /api/todos/:idFetch one200 + todo404
POST /api/todosCreate201 + Location400 invalid body
PATCH /api/todos/:idPartial update200 + updated400 / 404
DELETE /api/todos/:idRemove204 no body404

Every response body is JSON, every error uses the same envelope — { "error": { "code": "not_found", "message": "…" } } — and every status comes from the table in chapter 10. That consistency is what makes an API pleasant to use.

The in-memory store

Start simple: an array behind a couple of functions, kept in src/server.js for now.

let todos = [
  { id: 1, title: 'Design the API contract', done: true, createdAt: '2026-10-03T09:00:00.000Z' },
  { id: 2, title: 'Persist the data', done: false, createdAt: '2026-10-03T09:05:00.000Z' },
];
let nextId = 3;

const allTodos = () => todos;
const findTodo = (id) => todos.find((t) => t.id === id);

Plain functions over shared state are enough — but they are already an interface, which is exactly the seam chapter 16 will swap for file storage without touching the routes.

Read routes

app.get('/api/todos', (req, res) => {
  res.json({ count: allTodos().length, todos: allTodos() });
});

app.get('/api/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const todo = Number.isInteger(id) ? findTodo(id) : undefined;
  if (!todo) {
    return res.status(404).json({ error: { code: 'not_found', message: 'No todo with that id' } });
  }
  res.json(todo);
});
curl -s http://localhost:3000/api/todos
curl -i -s http://localhost:3000/api/todos/99

Expected output:

{"count":2,"todos":[{"id":1,"title":"Design the API contract","done":true,"createdAt":"2026-10-03T09:00:00.000Z"},{"id":2,"title":"Persist the data","done":false,"createdAt":"2026-10-03T09:05:00.000Z"}]}
HTTP/1.1 404 Not Found
{"error":{"code":"not_found","message":"No todo with that id"}}

Converting the id with Number() first means /api/todos/abc becomes a clean 404 instead of a silent mismatch — validate before you look up.

Create

app.post('/api/todos', (req, res) => {
  const { title, done = false } = req.body ?? {};
  if (typeof title !== 'string' || title.trim() === '') {
    return res.status(400).json({ error: { code: 'invalid_body', message: 'title is required' } });
  }
  if (typeof done !== 'boolean') {
    return res.status(400).json({ error: { code: 'invalid_body', message: 'done must be boolean' } });
  }

  const todo = { id: nextId++, title: title.trim(), done, createdAt: new Date().toISOString() };
  todos.push(todo);
  res.status(201).location(`/api/todos/${todo.id}`).json(todo);
});
curl -s -X POST http://localhost:3000/api/todos \
  -H 'Content-Type: application/json' -d '{"title":"Write chapter 15"}'

Expected output: {"id":3,"title":"Write chapter 15","done":false,"createdAt":"2026-10-03T09:31:00.000Z"} with status 201 and a Location: /api/todos/3 header. Reject with 400 (malformed intent) rather than 422 or 500 — the client sent something structurally wrong, and chapter 14's error middleware can catch anything that slips through.

Update and delete

app.patch('/api/todos/:id', (req, res) => {
  const todo = findTodo(Number(req.params.id));
  if (!todo) return res.status(404).json({ error: { code: 'not_found', message: 'No todo' } });

  const { title, done } = req.body ?? {};
  if (title !== undefined && (typeof title !== 'string' || !title.trim())) {
    return res.status(400).json({ error: { code: 'invalid_body', message: 'bad title' } });
  }
  if (done !== undefined && typeof done !== 'boolean') {
    return res.status(400).json({ error: { code: 'invalid_body', message: 'bad done' } });
  }

  if (title !== undefined) todo.title = title.trim();
  if (done !== undefined) todo.done = done;
  res.json(todo);
});

app.delete('/api/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const before = todos.length;
  todos = todos.filter((t) => t.id !== id);
  if (todos.length === before) {
    return res.status(404).json({ error: { code: 'not_found', message: 'No todo' } });
  }
  res.status(204).end();
});

Expected output: a valid PATCH answers 200 with the updated object; DELETE answers 204 with an empty body — nothing to serialise when deletion succeeds. PATCH only touches fields that were sent, which is the difference between PATCH and PUT from chapter 10.

Common mistakes

  • 200 for created or failed — 201 and 4xx exist precisely so clients can branch on status alone.
  • Skipping id coercion — '1' === 1 is false; unconverted params cause phantom 404s.
  • Validating after mutation — corrupt state written before the check; validate first, mutate second.
  • Returning 204 with a body — protocol-wise the body must be empty; some clients throw.
  • Not awaiting — coming next chapter — once storage becomes asynchronous, every store call needs await or you will read stale data.

Practice checklist

  1. Reproduce the full contract with curl: list, fetch, create, patch, delete — plus each error case.
  2. Add GET /api/todos?done=false filtering to the read route.
  3. Return 400 for an id that is not a number instead of 404, and update your client expectations.
  4. Introduce a shared sendError(res, status, code, message) helper and use it in every route.
  5. Kill and restart the server, then GET the list — watch your data vanish (the motivation for chapter 16).

The API works until the process restarts. Next: Persisting Data with a JSON Store.

Quiz: Building a REST API

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.