HTTP-запрос — это текстовое сообщение из метода, адреса, заголовков и необязательного тела; ответ — из статус-кода, заголовков и тела. JSON — формат, в котором тело обычно передаёт данные: объекты, массивы, строки в двойных кавычках, числа, true, false и null. Чтобы уверенно работать с API, нужно понимать три вещи: что означает метод (безопасен ли он и можно ли его повторить), что говорит класс статус-кода и какой Content-Type описывает тело.
После материала вы сможете
- разобрать HTTP-запрос и ответ на части: метод, адрес, заголовки, тело, статус-код
- объяснить, какие методы безопасны и идемпотентны по RFC 9110 и почему POST нельзя слепо повторять
- отправить GET и POST с JSON через curl и fetch и проверить статус ответа, а не только сам факт ответа
- найти причину ошибки JSON.parse и предсказать, что JSON.stringify потеряет при сериализации
Что знать заранее
Проверено на
Node.js 24 LTS (24.18)curl 8.21HTTP/1.1JSON (RFC 8259)
Сверено с документацией: 11 октября 2026 г.
Содержание
Из чего состоят запрос и ответ
В уроке о том, как устроен сайт, запрос был «официантом» между браузером и сервером. Здесь заглянем внутрь: что именно официант несёт. HTTP (HyperText Transfer Protocol) — протокол обмена сообщениями: клиент отправляет запрос, сервер возвращает ответ. Клиентом может быть браузер, мобильное приложение, другой сервер или утилита curl в терминале.
Запрос в HTTP/1.1 — это обычный текст. Первая строка содержит метод, адрес (request target) и версию протокола. Дальше идут заголовки — пары «имя: значение», каждая на своей строке. Затем пустая строка, а после неё — необязательное тело. Так выглядит запрос, который создаёт заказ:
POST /api/orders HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Content-Length: 27
{"item": "latte", "qty": 1}Ответ устроен так же, только первая строка другая: версия протокола, трёхзначный статус-код и короткая текстовая фраза, например HTTP/1.1 201 Created. Фраза носит справочный характер — программы ориентируются на число.
Методы: безопасность и идемпотентность
Метод сообщает серверу, чего хочет клиент. Стандарт RFC 9110 вводит два свойства, от которых зависит поведение браузеров, прокси и вашего собственного кода. Безопасный метод по смыслу только читает данные и не просит сервер ничего менять. Идемпотентный метод даёт тот же итог на сервере, сколько бы раз одинаковый запрос ни повторили.
GET
- Для чего
- Получить представление ресурса
- Безопасный
- Да
- Идемпотентный
- Да
HEAD
- Для чего
- То же, что GET, но только заголовки
- Безопасный
- Да
- Идемпотентный
- Да
POST
- Для чего
- Передать данные на обработку, создать запись
- Безопасный
- Нет
- Идемпотентный
- Нет
PUT
- Для чего
- Создать или полностью заменить ресурс по адресу
- Безопасный
- Нет
- Идемпотентный
- Да
PATCH
- Для чего
- Частично изменить ресурс (RFC 5789)
- Безопасный
- Нет
- Идемпотентный
- Нет
DELETE
- Для чего
- Удалить ресурс
- Безопасный
- Нет
- Идемпотентный
- Да
Идемпотентность — практическое свойство, а не теория. RFC 9110 прямо объясняет: если соединение оборвалось до ответа, клиент может повторить PUT или DELETE, потому что повтор не изменит итог. С POST так нельзя: повтор создаст второй заказ или спишет деньги дважды. Поэтому платёжные API добавляют к POST свои механизмы защиты от дублей, а формы блокируют повторную отправку.
Статус-коды и заголовки, которые нужно знать
Статус-код — трёхзначное число от 100 до 599. Первая цифра задаёт класс ответа, и по RFC 9110 клиент обязан понимать хотя бы класс: незнакомый код 471 он должен обработать как 400. Отсюда правило для своего кода: сначала проверяйте класс, потом конкретные значения.
2xx
- Что значит
- Успех: запрос получен, понят и принят
- Когда встречается
- 200 OK — данные в теле; 201 Created — запись создана, адрес в Location; 204 No Content — успех без тела
3xx
- Что значит
- Нужно дополнительное действие
- Когда встречается
- 301, 302, 308 — ресурс по другому адресу
400
- Что значит
- Сервер не может обработать запрос из-за ошибки клиента
- Когда встречается
- Тело — не корректный JSON
401
- Что значит
- Нет действующих учётных данных
- Когда встречается
- Токен не передан или истёк
404
- Что значит
- Ресурс не найден
- Когда встречается
- Заказа с таким id нет
415
- Что значит
- Формат тела не поддерживается
- Когда встречается
- Отправили JSON без
Content-Type: application/json
422
- Что значит
- Синтаксис верный, но данные не проходят правила
- Когда встречается
- Количество товара равно нулю
5xx
- Что значит
- Сервер не смог выполнить корректный запрос
- Когда встречается
- 500 — необработанная ошибка в коде сервера
Заголовки описывают сообщение, а не данные в нём. Имена заголовков не зависят от регистра: Content-Type и content-type — одно и то же. Node.js в req.headers приводит все имена к нижнему регистру, поэтому в коде сервера читают req.headers["content-type"].
Content-Type— формат тела:application/json,text/html; charset=utf-8. Если заголовка нет, получатель по RFC 9110 может считать тело просто байтами или угадывать тип — на это не стоит полагаться.Content-Length— размер тела в байтах, не в символах: кириллическая буква в UTF-8 занимает два байта.Authorization— учётные данные в виде «схема значение», напримерBearer YOUR_TOKEN. Такой токен даёт доступ любому, у кого он оказался, поэтому передавать его можно только по HTTPS.Cache-Control— правила кэширования.no-storeзапрещает сохранять ответ,no-cacheразрешает хранить, но требует перепроверки перед каждым использованием,max-age=N— ответ свежий N секунд.Location— адрес созданного ресурса в ответе 201 или новый адрес при перенаправлении.
Практика: свой API на node:http, запросы через curl и fetch
Лучший способ увидеть HTTP — поднять маленький сервер и отправить ему запросы. Понадобится только Node.js 24: модуль node:http встроен, внешних пакетов нет. Сервер хранит заказы в памяти, отдаёт заказ по GET и создаёт новый по POST, проверяя заголовок, синтаксис JSON и сами данные — каждой проверке соответствует свой статус-код.
import { createServer } from "node:http";
const orders = new Map([[1, { id: 1, item: "cappuccino", qty: 2 }]]);
let nextId = 2;
function sendJson(res, status, data, headers = {}) {
const body = JSON.stringify(data);
res.writeHead(status, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": Buffer.byteLength(body),
...headers,
});
res.end(body);
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
const server = createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
console.log(req.method, url.pathname, req.headers["content-type"] ?? "-");
const match = url.pathname.match(/^\/api\/orders\/(\d+)$/);
if (req.method === "GET" && match) {
const order = orders.get(Number(match[1]));
if (!order) return sendJson(res, 404, { error: "Order not found" });
return sendJson(res, 200, order, { "Cache-Control": "no-store" });
}
if (req.method === "POST" && url.pathname === "/api/orders") {
if (!req.headers["content-type"]?.startsWith("application/json")) {
return sendJson(res, 415, { error: "Expected Content-Type: application/json" });
}
let input;
try {
input = JSON.parse(await readBody(req));
} catch {
return sendJson(res, 400, { error: "Request body is not valid JSON" });
}
if (typeof input?.item !== "string" || !Number.isInteger(input.qty) || input.qty < 1) {
return sendJson(res, 422, { error: "Fields required: item (string), qty (integer >= 1)" });
}
const order = { id: nextId++, item: input.item, qty: input.qty };
orders.set(order.id, order);
return sendJson(res, 201, order, { Location: `/api/orders/${order.id}` });
}
sendJson(res, 404, { error: "Not found" });
});
server.listen(8080, "127.0.0.1", () => {
console.log("API: http://127.0.0.1:8080");
});Запустите node server.mjs и откройте второй терминал. Ключ -s у curl убирает индикатор прогресса, -i показывает статусную строку и заголовки ответа, -X задаёт метод, -H добавляет заголовок, -d передаёт тело. Команды ниже — для bash (macOS, Linux, Git Bash в Windows): в PowerShell кавычки экранируются иначе. Вывод — реальный, с нашего запуска:
$ curl -si http://127.0.0.1:8080/api/orders/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 36
Cache-Control: no-store
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"id":1,"item":"cappuccino","qty":2}
$ curl -si -X POST http://127.0.0.1:8080/api/orders -H 'Content-Type: application/json' -d '{"item": "latte", "qty": 1}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 31
Location: /api/orders/2
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"id":2,"item":"latte","qty":1}
$ curl -si -X POST http://127.0.0.1:8080/api/orders -H 'Content-Type: application/json' -d "{'item': 'latte'}"
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 42
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"error":"Request body is not valid JSON"}
$ curl -si -X POST http://127.0.0.1:8080/api/orders -d '{"item": "latte", "qty": 1}'
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json; charset=utf-8
Content-Length: 51
Date: Sun, 11 Oct 2026 21:56:01 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"error":"Expected Content-Type: application/json"}Обратите внимание на последний запрос. Без -H curl сам ставит Content-Type: application/x-www-form-urlencoded — так в журнале нашего сервера и записалось. Тело при этом содержит JSON, но сервер, как и положено, верит заголовку и отвечает 415. Это одна из самых частых причин «сервер не видит мои данные».
Теперь то же из кода. fetch встроен в Node.js начиная с 18-й версии и стабилен с 21-й; в браузере он работает так же. Перезапустите сервер, чтобы нумерация заказов началась заново, и выполните клиент:
const API = "http://127.0.0.1:8080/api";
async function request(method, path, body) {
const res = await fetch(API + path, {
method,
headers: body === undefined ? {} : { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const data = await res.json();
console.log(method, path, "→", res.status, res.ok ? "ok" : "error", data);
return data;
}
await request("GET", "/orders/1");
const created = await request("POST", "/orders", { item: "latte", qty: 1 });
await request("GET", `/orders/${created.id}`);
await request("POST", "/orders", { item: "latte", qty: 0 });
await request("GET", "/orders/999");$ node client.mjs
GET /orders/1 → 200 ok { id: 1, item: 'cappuccino', qty: 2 }
POST /orders → 201 ok { id: 2, item: 'latte', qty: 1 }
GET /orders/2 → 200 ok { id: 2, item: 'latte', qty: 1 }
POST /orders → 422 error { error: 'Fields required: item (string), qty (integer >= 1)' }
GET /orders/999 → 404 error { error: 'Order not found' }JSON: синтаксис, JSON.parse и JSON.stringify
JSON (JavaScript Object Notation) описан в RFC 8259. Значение в JSON — это объект, массив, число, строка или одно из трёх слов: true, false, null. Правила строже, чем в JavaScript: строки и имена полей только в двойных кавычках, без завершающих запятых, без комментариев, числа без ведущих нулей, а NaN и Infinity запрещены. Между системами JSON передают в кодировке UTF-8, медиатип — application/json.
В JavaScript с JSON работают две функции. JSON.stringify превращает значение в строку для отправки, JSON.parse — строку обратно в значение. Обе умеют молча терять данные или бросать исключения. Этот скрипт собирает типичные ловушки; ниже — реальный вывод Node.js 24:
const order = {
id: 7,
createdAt: new Date("2026-10-11T09:30:00Z"),
comment: undefined,
discount: NaN,
format() {},
};
console.log(JSON.stringify(order));
console.log(JSON.stringify({ items: ["latte", undefined] }));
console.log(JSON.stringify({ id: 1, tags: ["new"] }, null, 2));
const broken = [
"{'item': 'latte'}",
'{"item": "latte",}',
'{"qty": 012}',
"",
];
for (const text of broken) {
try {
JSON.parse(text);
} catch (error) {
console.log(error.name + ":", error.message);
}
}
console.log(JSON.parse('{"id": 9007199254740993}').id);
try {
JSON.stringify({ total: 10n });
} catch (error) {
console.log(error.name + ":", error.message);
}
const a = { name: "a" };
a.self = a;
try {
JSON.stringify(a);
} catch (error) {
console.log(error.name + ":", error.message.split("\n")[0]);
}$ node json-pitfalls.mjs
{"id":7,"createdAt":"2026-10-11T09:30:00.000Z","discount":null}
{"items":["latte",null]}
{
"id": 1,
"tags": [
"new"
]
}
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
SyntaxError: Expected double-quoted property name in JSON at position 17 (line 1 column 18)
SyntaxError: Unexpected number in JSON at position 9 (line 1 column 10)
SyntaxError: Unexpected end of JSON input
9007199254740992
TypeError: Do not know how to serialize a BigInt
TypeError: Converting circular structure to JSONПоля comment и format исчезли
- Почему
undefinedи функции в объекте пропускаются, в массиве становятсяnull- Что делать
- Передавайте
nullявно, если поле должно быть
discount стал null
- Почему
NaNиInfinityв JSON недопустимы и сериализуются какnull- Что делать
- Проверяйте числа до отправки
Дата стала строкой
- Почему
- У
Dateесть методtoJSON, он возвращает строку ISO 8601 - Что делать
- При разборе превращайте строку обратно в
Dateсами
SyntaxError при разборе
- Почему
- Одинарные кавычки, завершающая запятая, ведущий ноль или пустая строка
- Что делать
- Оборачивайте
JSON.parseв try/catch и отвечайте 400
9007199254740993 стало …992
- Почему
- Числа JSON разбираются в 64-битные числа с плавающей точкой; RFC 8259 гарантирует точность только для целых до 2^53 − 1
- Что делать
- Большие идентификаторы и суммы передавайте строками
TypeError у BigInt и циклической ссылки
- Почему
- Для них нет правила сериализации
- Что делать
- Преобразуйте значение заранее или используйте replacer
Запросы в DevTools и CORS в двух абзацах
Откройте вкладку Network
В Chrome или Edge — F12, затем Network. Обновите страницу или нажмите кнопку, которая отправляет запрос. Фильтр Fetch/XHR оставит только запросы к API.
Посмотрите заголовки
Кликните по запросу. На вкладке Headers — адрес, метод, статус-код, заголовки запроса и ответа.
Посмотрите тело
Payload — что отправил клиент, Response — сырой ответ сервера, Preview — тот же ответ в удобном виде.
Повторите в терминале
Правый клик по запросу → Copy → Copy as cURL. Получится команда curl с теми же заголовками: удобно, чтобы воспроизвести ошибку вне браузера и показать её бэкенд-разработчику.
Если тот же запрос работает в curl, но падает в браузере с ошибкой в консоли, почти наверняка дело в CORS (Cross-Origin Resource Sharing). Браузер по политике одного источника (same-origin policy) не даёт скрипту читать ответы с другого домена, порта или схемы, пока сервер явно не разрешит это заголовком Access-Control-Allow-Origin. Curl и серверный код этой политике не подчиняются — это защита пользователя в браузере, а не свойство HTTP.
Для «непростых» запросов — например, POST с Content-Type: application/json или с заголовком Authorization — браузер сначала отправляет предварительный запрос OPTIONS (preflight) и спрашивает, разрешены ли такой метод и заголовки. Подробности причины JavaScript не узнает: по соображениям безопасности они видны только в консоли браузера. Исправляют CORS на сервере, а не во фронтенде.
- Метод соответствует действию: чтение — GET, создание — POST, без изменений данных через GET
- У запроса с телом есть верный
Content-Type - Код проверяет
res.okилиres.status, прежде чем читать тело JSON.parseвходящих данных обёрнут в try/catch- Большие числа и деньги передаются строками
- Токены и ключи — только в заголовке
Authorization, только по HTTPS и не во фронтенд-коде
Как проверено5 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
- запущено, работает
server.mjs (node:http, GET/POST, 200/201/400/404/415/422)
node server.mjs на Node.js 24.18.0 (Windows 11), 127.0.0.1:8080; запросы curl 8.21.0 и client.mjs, вывод в статье — реальный из этого запуска (curl.out, client.out); журнал сервера подтвердил Content-Type: application/x-www-form-urlencoded при curl -d без -H
- запущено, работает
client.mjs (fetch)
node client.mjs против свежезапущенного server.mjs, Node.js 24.18.0; вывод вставлен без правок
- запущено, работает
curl-команды раздела «Практика»
выполнены в Git Bash (curl 8.21.0) против server.mjs; вывод, включая заголовок Date, — реальный
- запущено, работает
json-pitfalls.mjs
node json-pitfalls.mjs, Node.js 24.18.0; тексты SyntaxError/TypeError — реальные сообщения V8
- запущено, работает
Решение упражнения DELETE (фрагмент)
вставлен в копию server.mjs перед финальным 404; curl -X DELETE дважды: 204, затем 404
- не запускалось
Сырой HTTP-запрос (lang http)
иллюстрация формата сообщения, сверена с MDN HTTP messages; Content-Length 27 соответствует телу
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.