M Logo
๐Ÿ“ TypeScript from Zero ยท Part 36 of 50

TypeScript HTTP API with node:http: Routes, Typed JSON Helper, 404 and --watch

November 6, 2026

Welcome to episode 36 of TypeScript from Zero, a series for complete beginners (no JavaScript needed). Welcome back to TypeScript from zero! Now we put our tasks on the network. Node has an HTTP server built in, no library needed. Today: a server with routes, JSON answers, and a proper four oh four. A new file called server.ts in the tasks project. Every code block below is the exact code and output from the video, ready to copy.

Setup

We start in the terminal and open the project in VS Code:

bash
cd tasks
touch src/server.ts
code .

1. Helper

First a small helper. Send JSON takes the response object, a status number, and a body. It sets the header that says: this is JSON, writes the status, and sends the body as text. The body type is unknown, because anything can be sent. We also import our config from episode thirty five, for the port.

2. Server

Create server takes a function that runs for every request. We build a URL from the request, and load the tasks. A get request to slash tasks answers with the list. A get request to slash tasks slash a number answers with one task, or with four oh four, if there is no such task. Everything else is a four oh four too. At the end, listen starts the server on our port.

3. Run

We start the server with node --watch, which restarts it every time we save a file. Then we split the terminal, and use curl as the client. The list of tasks arrives as JSON. One task by id works. An id that does not exist gives four oh four, and the status line says so. And an unknown path gives four oh four as well.

src/server.ts:

ts
import { createServer } from "node:http";
import type { ServerResponse } from "node:http";
import { config } from "./config.ts";
import { loadTasks } from "./store.ts";

function sendJson(res: ServerResponse, status: number, body: unknown): void {
  res.writeHead(status, { "content-type": "application/json" });
  res.end(JSON.stringify(body));
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");
  const tasks = await loadTasks();
  if (req.method === "GET" && url.pathname === "/tasks") {
    sendJson(res, 200, tasks);
    return;
  }
  const match = url.pathname.match(/^\/tasks\/(\d+)$/);
  if (req.method === "GET" && match) {
    const id = Number(match[1]);
    const task = tasks.find((t) => t.id === id);
    if (task) {
      sendJson(res, 200, task);
    } else {
      sendJson(res, 404, { error: "no such task" });
    }
    return;
  }
  sendJson(res, 404, { error: "not found" });
});

server.listen(config.PORT, () => {
  console.log("Listening on http://localhost:" + config.PORT);
});
text
$ node src/client.ts
HTTP/1.1 200 OK
content-type: application/json
Date: Fri, 06 Nov 2026 10:06:03 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

[{"id":1,"title":"Buy milk","status":"todo"}]

{"id":1,"title":"Buy milk","status":"todo"}

HTTP/1.1 404 Not Found
content-type: application/json
Date: Fri, 06 Nov 2026 10:06:03 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"error":"no such task"}

HTTP/1.1 404 Not Found
content-type: application/json
Date: Fri, 06 Nov 2026 10:06:03 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"error":"not found"}
Listening on http://localhost:3000

4. Error

Types protect the server too. If we pass the status as a text, a mistake that is easy to make, tsc says: argument of type string is not assignable to parameter of type number. TS2345. Node would not mind: it accepts both. But a number is what the standard says.

src/server.ts:

ts
import { createServer } from "node:http";
import type { ServerResponse } from "node:http";
import { config } from "./config.ts";
import { loadTasks } from "./store.ts";

function sendJson(res: ServerResponse, status: number, body: unknown): void {
  res.writeHead(status, { "content-type": "application/json" });
  res.end(JSON.stringify(body));
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");
  const tasks = await loadTasks();
  if (req.method === "GET" && url.pathname === "/tasks") {
    sendJson(res, "200", tasks);
    return;
  }
  const match = url.pathname.match(/^\/tasks\/(\d+)$/);
  if (req.method === "GET" && match) {
    const id = Number(match[1]);
    const task = tasks.find((t) => t.id === id);
    if (task) {
      sendJson(res, 200, task);
    } else {
      sendJson(res, 404, { error: "no such task" });
    }
    return;
  }
  sendJson(res, 404, { error: "not found" });
});

server.listen(config.PORT, () => {
  console.log("Listening on http://localhost:" + config.PORT);
});
text
$ npx tsc --noEmit
src/server.ts:15:19 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

15     sendJson(res, "200", tasks);
                     ~~~~~

Found 1 error in src/server.ts:15

5. Fix

We write the number again, and tsc is quiet.

src/server.ts:

ts
import { createServer } from "node:http";
import type { ServerResponse } from "node:http";
import { config } from "./config.ts";
import { loadTasks } from "./store.ts";

function sendJson(res: ServerResponse, status: number, body: unknown): void {
  res.writeHead(status, { "content-type": "application/json" });
  res.end(JSON.stringify(body));
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");
  const tasks = await loadTasks();
  if (req.method === "GET" && url.pathname === "/tasks") {
    sendJson(res, 200, tasks);
    return;
  }
  const match = url.pathname.match(/^\/tasks\/(\d+)$/);
  if (req.method === "GET" && match) {
    const id = Number(match[1]);
    const task = tasks.find((t) => t.id === id);
    if (task) {
      sendJson(res, 200, task);
    } else {
      sendJson(res, 404, { error: "no such task" });
    }
    return;
  }
  sendJson(res, 404, { error: "not found" });
});

server.listen(config.PORT, () => {
  console.log("Listening on http://localhost:" + config.PORT);
});
bash
$ npx tsc --noEmit

Recap

Today: create server, a JSON helper, routes by method and path, the four oh four answers, and node --watch. Next episode: accepting data with a post request, and checking it with Zod.

ยฉ 2026 All Rights Reserved By Mahmudul Alam