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
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.
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.