Небольшой, но полный API
Пора собрать всё вместе: API пользователей с пятью классическими эндпоинтами —
POST /users создать · GET /users получить список · GET /users/{id} прочитать одного · PUT /users/{id} заменить · DELETE /users/{id} удалить.
Хранилище — это Map в памяти, поэтому пример остаётся самодостаточным. Node.js выполняет ваши обработчики один за другим в одном потоке, так что блокировки здесь не нужны; настоящий сервер заменил бы Map на базу данных, а форма обработчиков не изменилась бы.
Создание и список
POST читает входящий JSON из тела запроса, присваивает пользователю id, сохраняет его и отвечает 201 с созданной записью — вместе с id, чтобы клиент его узнал. GET возвращает всех в виде JSON-массива.
Тело приходит не целиком сразу, поэтому небольшая вспомогательная функция readBody собирает фрагменты с помощью for await, прежде чем их увидит JSON.parse.
POST /users и GET /users
Два создания, затем список: каждый POST отвечает 201 с сохранённым пользователем, а последний GET показывает обоих — состояние API росло с каждым запросом.
Вывод: программа на TypeScript и захваченный вывод
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
interface UserRecord {
id: number
name: string
}
// Собираем всё тело запроса в виде текста.
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("некорректный 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()
Чтение одного, замена, удаление
Маршруты с {id} дополняют набор: чтение отвечает 200 или 404, замена принимает новые данные и сохраняет id из URL, удаление отвечает 204. После удаления тот же GET, который раньше работал, честно возвращает 404 — ресурса больше нет.
GET, PUT и DELETE для /users/{id}
Полная жизнь одного ресурса: прочитать его, заменить (ответ содержит новое имя Ana Maria), удалить и не суметь прочитать — каждый шаг с правильным кодом состояния.
Вывод: программа на TypeScript и захваченный вывод
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: "пользователь не найден" }))
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 }
// Заменяем данные, но сохраняем id из 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()