← thecodex.expert · The Codex Family of Knowledge
Tier 3 · Upper-Intermediate · TypeScript Project

REST API

Build a JSON REST API with proper routes and methods — create, read, update, delete records over HTTP. The backbone of modern apps.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a REST API: a web service that lets clients create, read, update, and delete records (a to-do list, say) using standard HTTP methods (GET, POST, PUT, DELETE) and JSON. It teaches the conventions of REST — how the entire web of apps and services talks to itself.

Where this shows up: every mobile app backend, every single-page web app, every microservice, every public API (Stripe, Twitter, GitHub). REST is the lingua franca of the internet. Understanding it is essential to backend work.

2 How to Think About It

Think about how REST maps actions to HTTP, before any code:

The plan — in plain English
1. Each record lives at a URL like /todos/3. → 2. The HTTP method says what to do: GET reads, POST creates, DELETE removes. → 3. The body carries JSON data. → 4. The server routes each request to the right handler and returns JSON. This method-plus-URL convention is REST.

GET

POST

PUT

DELETE

Request arrives

Which method?

Return records as JSON

Create a record

Update a record

Remove a record

Send JSON response

3 The Build — explained part by part

Here is the complete API. Read each part’s note below — you should understand the whole thing from the notes alone.

TypeScriptapi.ts
import * as http from "node:http";

export interface Todo {
  id: number;
  task: string;
  done: boolean;
}

export type Store = Map<number, Todo>;

export function createTodo(store: Store, nextId: number, task: string): Todo {
  const todo: Todo = { id: nextId, task, done: false };
  store.set(nextId, todo);
  return todo;
}

export function deleteTodo(store: Store, todoId: number): boolean {
  return store.delete(todoId);
}

function send(res: http.ServerResponse, code: number, data: unknown): void {
  res.writeHead(code, { "Content-Type": "application/json" });
  res.end(JSON.stringify(data));
}

function main(): void {
  const todos: Store = new Map();
  let nextId = 1;

  const server = http.createServer((req, res) => {
    if (req.method === "GET" && req.url === "/todos") {
      // GET /todos -> list every todo.
      send(res, 200, [...todos.values()]);
      return;
    }

    if (req.method === "POST" && req.url === "/todos") {
      // POST /todos -> create a todo from the JSON body.
      let body = "";
      req.on("data", (chunk) => (body += chunk));
      req.on("end", () => {
        const parsed = JSON.parse(body) as { task: string };
        const todo = createTodo(todos, nextId, parsed.task);
        nextId += 1;
        send(res, 201, todo);
      });
      return;
    }

    if (req.method === "DELETE" && req.url?.startsWith("/todos/")) {
      // DELETE /todos/3 -> remove that todo.
      const todoId = Number(req.url.split("/").pop());
      if (deleteTodo(todos, todoId)) {
        send(res, 200, { deleted: todoId });
      } else {
        send(res, 404, { error: "not found" });
      }
      return;
    }

    send(res, 404, { error: "not found" });
  });

  server.listen(8000, "localhost");
}

if (require.main === module) {
  main();
}
⚠ No in-browser playground here
Running real, type-checked TypeScript in the browser needs either a full copy of the compiler or a third-party CDN script — the same kind of external dependency this site avoids relying on for a core teaching example. Copy the code below and run it with Node on your own machine instead; the “Run It” section explains exactly how.
What each part does — in plain words
interface Todo / type Store = Map<number, Todo> — a Todo always has an id, a task, and a done flag; the in-memory database (Store) is a Map from numeric ID to Todo, Node’s nearest equivalent to Python’s plain dict used the same way.

createTodo / deleteTodo — pulled out of the server exactly like the Python version’s helper functions, so the core logic is tested without starting a real server or making a real HTTP request.

http.createServer((req, res) => { ... }) — Node’s built-in HTTP server, no framework required. Each request is routed by hand, checking req.method and req.url — the same manual routing style as the Python version’s do_GET/do_POST/do_DELETE methods.

req.on("data", ...) / req.on("end", ...) — an incoming request body arrives in chunks as a stream; we collect every chunk and only parse the JSON once the 'end' event says the whole body has arrived.
Common mistakes — and how to avoid them
✗ Calling JSON.parse(body) before the 'end' event fires.
✓ The request body streams in over possibly many 'data' events; parsing too early gets a truncated, invalid JSON string. Always parse inside the 'end' handler.
✗ Using a plain object ({}) for the todo store and numeric-looking string keys.
✓ A Map<number, Todo> keeps the keys genuinely numeric and makes .has()/.delete() explicit, rather than relying on JavaScript’s implicit string coercion of object keys.
✗ Forgetting a default/catch-all route.
✓ Any request that matches none of the if blocks above falls through to a final send(res, 404, ...), so the server always responds instead of hanging.

4 Test & Prove Each Part

How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.

Creating stores a todo with its ID
Deleting removes the todo
Deleting a missing todo returns false
TypeScriptapi.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createTodo, deleteTodo, type Store } from "./rest-api";

test("creating stores a todo with its ID", () => {
  const store: Store = new Map();
  const todo = createTodo(store, 1, "Buy milk");
  assert.equal(todo.id, 1);
  assert.equal(store.get(1)?.task, "Buy milk");
});

test("deleting removes the todo", () => {
  const store: Store = new Map([[1, { id: 1, task: "x", done: false }]]);
  assert.equal(deleteTodo(store, 1), true);
  assert.equal(store.has(1), false);
});

test("deleting a missing todo returns false", () => {
  assert.equal(deleteTodo(new Map(), 99), false);
});

Compile with npx tsc then run node --test api.test.js. createTodo/deleteTodo operate on a plain Map, so the tests never start the real server or make an HTTP request — that is verified separately, by hand, with curl (see below).

5 The Interface

The API’s endpoints, documented like any professional REST service.

INPUTPOST /todoscreate a todo (JSON body)
What it expects
{"task": "Buy milk"}
OUTPUTResponse (201)the created todo
What it returns
{"id":1,"task":"Buy milk","done":false}

6 Run It & Automate It

Save the code as api.ts, compile with npx tsc, and run with node api.js — or run it directly with npx tsx api.ts. It listens on localhost:8000.

Run it locally
npx tsc api.ts && node api.js
Starts the server; leave it running in one terminal and talk to it with curl from another.

A CI tool like Jenkins runs the type-checker and tests automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
$ curl http://localhost:8000/todos
[]
$ curl -X POST http://localhost:8000/todos -d '{"task":"Buy milk"}'
{"id":1,"task":"Buy milk","done":false}
$ curl http://localhost:8000/todos
[{"id":1,"task":"Buy milk","done":false}]
If it breaks — how to fix it
🚨 EADDRINUSE: address already in use :::8000
Another process (often a previous run you forgot to stop) is already listening on port 8000. Stop it, or change the port in server.listen(8000, ...).
🚨 POST hangs and never responds
Check that the request actually includes a body and a Content-Length — and that every code path inside the handler eventually calls send(...), since res.end() is what finishes the response.
GroovyJenkinsfile
// Jenkinsfile &mdash; runs the type-checker and tests automatically every time the code changes.
pipeline {
    agent any                                  // run on any available machine

    stages {
        stage('Get the code') {
            steps { checkout scm }             // download the latest code
        }
        stage('Set up Node') {
            steps {
                sh 'node --version'                              // confirm Node is installed
                sh 'npm install -D typescript @types/node'       // zero runtime deps &mdash; just the compiler and its Node types
            }
        }
        stage('Type-check and test') {
            steps {
                sh 'npx tsc --noEmit'                 // catch type errors before anything runs
                sh 'npx tsc'                           // compile to plain JavaScript
                sh 'node --test api.test.js'            // Node's built-in test runner, no extra install needed
            }
        }
    }

    post {
        success { echo 'All tests passed.' }
        failure { echo 'A test failed &mdash; look above.' }
    }
}
🎯 Try this next — make it yours

You have a working rest api. Extend it:

  1. Add a PUT /todos/:id route. Let an existing todo’s task or done be updated. (Teaches: parsing a body for an update instead of a create.)
  2. Validate the request body. Reject a POST with no task field with a 400. (Teaches: a type guard on untrusted JSON before trusting its shape.)
  3. Persist to a file. Save the Store to JSON on every change, as the to-do list and CLI task manager projects do. (Teaches: combining this project with another on this page.)
  4. Add query-string filtering. Support GET /todos?done=true. (Teaches: URL and URLSearchParams for parsing query strings.)
What you learned
You learned to model a resource with an interface, route raw HTTP requests by hand with Node’s built-in http module, and why a streamed request body must be fully collected before JSON.parse. Related reference: Interfaces vs. Types, Typing Async Code.