All field notes

How to Build an API from Scratch (Node + Express)

A short, honest tutorial for building a working JSON API. Routing, validation, error handling, and what real production work looks like beyond the toy version.

On this page7 sections
  1. 01Prereqs
  2. 021. Set up the project
  3. 032. Write the server
  4. 043. Run it
  5. 054. Try it
  6. 065. What's missing for production
  7. 07Test the endpoints without writing a client

A short, honest tutorial. We'll build a working JSON API for a tiny todo list. By the end you'll understand routing, request parsing, validation, and how to expose it to the world.

Prereqs

  • Node 18+
  • A code editor

1. Set up the project

mkdir todo-api && cd todo-api
npm init -y
npm install express zod
npm install -D typescript tsx @types/express @types/node
npx tsc --init

2. Write the server

Create src/server.ts:

import express from "express";
import { z } from "zod";

const app = express();
app.use(express.json());

const todos: { id: string; text: string; done: boolean }[] = [];

const TodoInput = z.object({ text: z.string().min(1) });

app.get("/todos", (_req, res) => res.json(todos));

app.post("/todos", (req, res) => {
  const parsed = TodoInput.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: parsed.error.flatten() });
  }
  const todo = { id: crypto.randomUUID(), text: parsed.data.text, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

app.patch("/todos/:id", (req, res) => {
  const todo = todos.find((t) => t.id === req.params.id);
  if (!todo) return res.status(404).json({ error: "not found" });
  if (typeof req.body.done === "boolean") todo.done = req.body.done;
  res.json(todo);
});

app.delete("/todos/:id", (req, res) => {
  const i = todos.findIndex((t) => t.id === req.params.id);
  if (i === -1) return res.status(404).json({ error: "not found" });
  todos.splice(i, 1);
  res.status(204).end();
});

app.listen(3000, () => console.log("listening on :3000"));

3. Run it

npx tsx src/server.ts

4. Try it

curl -X POST http://localhost:3000/todos \
  -H 'Content-Type: application/json' \
  -d '{"text":"ship the API"}'

You should see your todo come back with an id.

5. What's missing for production

This is a toy. Real APIs add:

  • A real database (Postgres + Prisma is the default)
  • Auth (start with a JWT middleware)
  • Rate limiting
  • Request logging
  • An OpenAPI spec so other people can use it
  • Tests

Test the endpoints without writing a client

curl works but it's painful for anything past three endpoints. PreMan imports your routes (or your OpenAPI spec) and gives you a button per endpoint. Every call is saved, so you can rerun your last 50 requests after a refactor and see what changed.

→ Drop your todo API into PreMan

Bring the loop to your API

Catch the regression. Open a verified fix.

Join the waitlist to see which users a release may affect, monitor endpoints in production, and prepare a reviewable fix PR when something breaks.