http.createServer() создаёт веб-сервер из одной функции, обработчика запросов. Node вызывает её на каждый входящий запрос с двумя объектами: req (метод, URL и заголовки, которые прислал клиент) и res (то, что вы отправляете в ответ). Задайте в res код статуса и заголовки, затем вызовите res.end() с телом ответа. server.listen(3000) начинает принимать соединения на порту 3000.
Первый запрос получает ответ You asked for GET /hello?name=Ada, а в Console выводятся тот же метод и URL. Отправьте DELETE /anything из строки запроса: ответит тот же обработчик, потому что у простого сервера http нет маршрутов. Каждый запрос проходит через одну функцию.
Отправить JSON и задать код статуса
res.writeHead(status, headers) задаёт код статуса и заголовки одним вызовом. Для JSON укажите Content-Type: application/json и отправьте результат JSON.stringify(): res.end() принимает только строку или Buffer.
GET /users?limit=1 возвращает [{"id":1,"name":"Ada"}]. req.url это путь вместе со строкой запроса, поэтому new URL(req.url, 'http://localhost') делит его на pathname и searchParams (база нужна только потому, что URL требует абсолютный адрес). Отправьте GET /nothing, и статус станет 404.
Маршруты через switch
Небольшому API хватит switch по методу и пути. Соберите ключ вроде 'GET /users' и дайте каждому маршруту свой case:
Отправьте POST /users с телом {"name": "Grace"}: в ответ придёт 201 с {"id":2,"name":"Grace"}, а GET /users теперь покажет обоих пользователей. Отправьте тот же POST с телом hello, и получите 400 с {"error":"Body must be JSON"}.
Прочитать тело POST запроса
Node не разбирает тело за вас. req это readable stream: тело приходит кусками через события data, а когда оно получено целиком, срабатывает end. readBody() выше собирает куски в строку и возвращает промис, так что обработчик может сделать await. Поскольку req поддерживает асинхронную итерацию, этот цикл делает то же самое:
let data = '';
for await (const chunk of req) data += chunk;
const body = JSON.parse(data);
Настоящий сервер ещё ограничивает размер (перестаёт читать и отвечает 413, когда data.length превышает лимит) и проверяет req.headers['content-type'] перед вызовом JSON.parse.
Частые ошибки
Забыли res.end(). Ветка, которая задаёт статус, но не завершает ответ, заставляет клиента ждать, пока не истечёт таймаут.
Заголовок после тела. Как только выполнился res.end() или первый res.write(), заголовки уже отправлены, и res.setHeader() бросает ERR_HTTP_HEADERS_SENT. Пишите return, когда отправляете ответ раньше времени, как в примерах.
Сравнение req.url с путём. req.url включает строку запроса, поэтому req.url === '/users' ложно для /users?limit=1. Сравнивайте pathname.
Когда лучше взять Express
Модуля http хватает для health check, приёмника вебхуков или чтобы понять, как работает HTTP. Когда нужны параметры маршрута вроде /users/:id, разбор JSON в теле, middleware и обработка ошибок, вы в итоге пишете свой маленький фреймворк. Express и есть такой фреймворк, построенный на тех же объектах req и res; в маршрутизации в Express показан тот же API с app.get() и app.post().
Часто задаваемые вопросы
Чем res.write отличается от res.end?
res.write() отправляет кусок тела и оставляет ответ открытым, так что его можно вызывать много раз. res.end() отправляет необязательный последний кусок и завершает ответ. В каждом ответе должен быть ровно один res.end().
Как остановить HTTP сервер на Node?
Вызовите server.close(). Он сразу перестаёт принимать новые соединения и вызывает свой callback, когда открытые соединения завершатся. Именно это нужно для аккуратного завершения по SIGTERM.
Как использовать HTTPS вместо модуля http?
Возьмите модуль https: https.createServer({ key, cert }, handler) принимает тот же обработчик плюс приватный ключ и сертификат. В продакшене TLS обычно завершает прокси вроде Nginx или хостинг, а ваш сервер на Node продолжает слушать обычный HTTP.
Какой порт должен слушать сервер на Node?
Любой свободный порт выше 1024 работает без прав администратора; при разработке часто берут 3000 и 8080. Хостинги обычно передают порт в переменной окружения, поэтому пишите server.listen(process.env.PORT || 3000).