A Small, Complete API
Time to assemble the pieces: a users API with the five classic endpoints —
POST /users create · GET /users list · GET /users/{id} read one · PUT /users/{id} replace · DELETE /users/{id} remove.
The store is a Map in memory, so the example stays self-contained. Node.js runs your handlers one after another on a single thread, so nothing here needs locking; a real server would swap the Map for a database, and the handlers would not change shape.
Create and List
POST reads the incoming JSON from the request body, gives the user an id, stores it, and answers 201 with the created record — id included, so the client learns it. GET returns everyone as a JSON array.
The body does not arrive all at once, so a small readBody helper collects the chunks with for await before JSON.parse sees them.
POST /users and GET /users
Two creations, then the list: each POST answers 201 with the stored user, and the final GET shows both — the API's state grew with each request.
Output: TypeScript file and captured output
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
interface UserRecord {
id: number
name: string
}
// Collect the whole request body as text.
async function readBody(req: IncomingMessage): Promise<string> {
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk as Buffer)
return Buffer.concat(chunks).toString()
}
const users = new Map<number, UserRecord>()
let nextId = 1
async function createUser(req: IncomingMessage, res: ServerResponse): Promise<void> {
let incoming: { name?: string }
try {
incoming = JSON.parse(await readBody(req))
} catch {
res.writeHead(400).end("invalid JSON\n")
return
}
const user: UserRecord = { id: nextId++, name: incoming.name ?? "" }
users.set(user.id, user)
res.writeHead(201, { "Content-Type": "application/json" })
res.end(JSON.stringify(user))
}
function listUsers(req: IncomingMessage, res: ServerResponse): void {
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify([...users.values()]))
}
async function main(): Promise<void> {
const server = createServer((req, res) => {
const url = new URL(req.url ?? "/", "http://localhost")
if (url.pathname === "/users" && req.method === "POST") {
createUser(req, res)
return
}
if (url.pathname === "/users" && req.method === "GET") {
listUsers(req, res)
return
}
res.writeHead(404).end()
})
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const base = `http://localhost:${(server.address() as { port: number }).port}`
for (const name of ["Ana", "Ben"]) {
const res = await fetch(`${base}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
})
console.log("POST /users ->", res.status, await res.text())
}
const list = await fetch(`${base}/users`)
console.log("GET /users ->", list.status, await list.text())
server.close()
}
main()
Read One, Replace, Delete
The {id} routes complete the set: read answers 200 or 404, replace takes the new data and keeps the id from the URL, delete answers 204. After the delete, the same GET that worked before honestly returns 404 — the resource is gone.
GET, PUT and DELETE on /users/{id}
The full life of one resource: read it, replace it (the response carries the new name Ana Maria), delete it, and fail to read it — every step with the correct status code.
Output: TypeScript file and captured output
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
interface UserRecord {
id: number
name: string
}
type RouteHandler = (
req: IncomingMessage,
res: ServerResponse,
params: Record<string, string>,
) => void | Promise<void>
interface Route {
method: string
pattern: string
handler: RouteHandler
}
function matchPath(pattern: string, path: string): Record<string, string> | undefined {
const want = pattern.split("/")
const got = path.split("/")
if (want.length !== got.length) return undefined
const params: Record<string, string> = {}
for (let i = 0; i < want.length; i++) {
if (want[i].startsWith("{") && want[i].endsWith("}")) {
params[want[i].slice(1, -1)] = got[i]
} else if (want[i] !== got[i]) {
return undefined
}
}
return params
}
function router(routes: Route[]) {
return (req: IncomingMessage, res: ServerResponse): void => {
const url = new URL(req.url ?? "/", "http://localhost")
for (const route of routes) {
const params = matchPath(route.pattern, url.pathname)
if (params !== undefined && route.method === req.method) {
route.handler(req, res, params)
return
}
}
res.writeHead(404).end()
}
}
async function readBody(req: IncomingMessage): Promise<string> {
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk as Buffer)
return Buffer.concat(chunks).toString()
}
const users = new Map<number, UserRecord>([[1, { id: 1, name: "Ana" }]])
const routes: Route[] = [
{
method: "GET",
pattern: "/users/{id}",
handler: (req, res, params) => {
res.setHeader("Content-Type", "application/json")
const user = users.get(Number(params.id))
if (user === undefined) {
res.writeHead(404).end(JSON.stringify({ error: "user not found" }))
return
}
res.end(JSON.stringify(user))
},
},
{
method: "PUT",
pattern: "/users/{id}",
handler: async (req, res, params) => {
const incoming = JSON.parse(await readBody(req)) as { name: string }
// Replace the data but keep the id from the URL.
const user: UserRecord = { id: Number(params.id), name: incoming.name }
users.set(user.id, user)
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify(user))
},
},
{
method: "DELETE",
pattern: "/users/{id}",
handler: (req, res, params) => {
users.delete(Number(params.id))
res.writeHead(204).end()
},
},
]
async function main(): Promise<void> {
const server = createServer(router(routes))
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const base = `http://localhost:${(server.address() as { port: number }).port}`
async function call(method: string, path: string, payload?: string): Promise<void> {
const res = await fetch(base + path, {
method,
headers: payload === undefined ? {} : { "Content-Type": "application/json" },
body: payload,
})
console.log(`${method.padEnd(6)} ${path} -> ${res.status} ${await res.text()}`.trimEnd())
}
await call("GET", "/users/1")
await call("PUT", "/users/1", JSON.stringify({ name: "Ana Maria" }))
await call("DELETE", "/users/1")
await call("GET", "/users/1")
server.close()
}
main()