HTTP REST API

HTTP (Hypertext Transfer Protocol) — протокол прикладного уровня для обмена данными между клиентом и сервером.

Клиент отправляет HTTP-запрос, а сервер возвращает HTTP-ответ:

Клиент → HTTP-запрос → Сервер
Клиент ← HTTP-ответ ← Сервер

В роли клиента могут выступать:

HTTPS — HTTP, работающий через защищённое TLS-соединение. HTTPS шифрует передаваемые данные и позволяет проверить подлинность сервера.

Пример HTTP-запроса:

GET /api/users/42 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: Bearer access-token

Пример HTTP-ответа:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, max-age=60

{
  "id": 42,
  "name": "Анна"
}

HTTP сам по себе не определяет бизнес-логику приложения. Он задаёт правила передачи запросов, ответов, заголовков, статусов и содержимого.

Содержание


Структура HTTP-запроса

HTTP-запрос состоит из:

  1. стартовой строки;
  2. заголовков;
  3. пустой строки;
  4. необязательного тела.
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json
Authorization: Bearer access-token

{
  "name": "Анна",
  "email": "anna@example.com"
}

Стартовая строка:

POST /api/users HTTP/1.1

Заголовки передают метаданные запроса:

Content-Type: application/json
Accept: application/json

Тело содержит данные, отправляемые серверу:

{
  "name": "Анна",
  "email": "anna@example.com"
}

У запросов GET и HEAD тело обычно не используется. Параметры передаются через путь или строку запроса:

GET /api/users?role=admin&page=2 HTTP/1.1

Структура HTTP-ответа

HTTP-ответ состоит из:

  1. строки статуса;
  2. заголовков;
  3. пустой строки;
  4. необязательного тела.
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/43

{
  "id": 43,
  "name": "Анна",
  "email": "anna@example.com"
}

Строка статуса:

HTTP/1.1 201 Created

Заголовок Location указывает адрес созданного ресурса:

Location: /api/users/43

HTTP-методы

HTTP-метод описывает действие, которое клиент хочет выполнить над ресурсом.

Метод Назначение Безопасный Идемпотентный
GET Получить ресурс Да Да
HEAD Получить только заголовки Да Да
POST Создать ресурс или запустить операцию Нет Обычно нет
PUT Полностью создать или заменить ресурс Нет Да
PATCH Частично изменить ресурс Нет Не обязательно
DELETE Удалить ресурс Нет Да
OPTIONS Получить параметры взаимодействия Да Да

Безопасный метод не должен изменять состояние ресурса.

Идемпотентный метод при повторном выполнении должен приводить сервер к тому же итоговому состоянию, что и однократное выполнение.

Ответы на повторные идемпотентные запросы могут отличаться. Например, первый DELETE может вернуть 204 No Content, а второй — 404 Not Found, но удалённый ресурс в обоих случаях остаётся удалённым.

GET

Получает ресурс или коллекцию ресурсов:

GET /api/users/42 HTTP/1.1
Accept: application/json

Ответ:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 42,
  "name": "Анна"
}

Получение коллекции:

GET /api/users HTTP/1.1

Фильтрация и пагинация:

GET /api/users?role=admin&page=2&limit=20 HTTP/1.1

GET не следует использовать для операций, изменяющих данные:

Нежелательно:
GET /api/users/42/delete

Предпочтительно:
DELETE /api/users/42

Работает аналогично GET, но сервер не возвращает тело ответа:

HEAD /api/files/manual.pdf HTTP/1.1

Ответ:

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 245810
ETag: "file-v7"

HEAD можно использовать для проверки:

POST

Обычно создаёт новый ресурс в коллекции:

POST /api/users HTTP/1.1
Content-Type: application/json

{
  "name": "Анна",
  "email": "anna@example.com"
}

Ответ:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/43

{
  "id": 43,
  "name": "Анна",
  "email": "anna@example.com"
}

POST также может запускать операцию, которая не укладывается в обычное создание ресурса:

POST /api/reports/42/export HTTP/1.1

Повторный POST может создать несколько ресурсов или несколько раз выполнить операцию. Если повтор запроса возможен из-за сетевых сбоев, API может поддерживать ключ идемпотентности:

POST /api/payments HTTP/1.1
Idempotency-Key: 9cf3a451-2194-4c70-a2e4-57b5e19c6ba7
Content-Type: application/json

Сервер связывает результат с ключом и не выполняет одну операцию повторно.

PUT

Полностью заменяет ресурс по известному адресу:

PUT /api/users/42 HTTP/1.1
Content-Type: application/json

{
  "name": "Анна Петрова",
  "email": "anna@example.com",
  "role": "editor"
}

В классической семантике PUT передаёт полное новое представление ресурса. Пропущенные поля могут быть удалены или сброшены.

В некоторых API PUT также создаёт ресурс, если по указанному адресу его ещё нет.

PATCH

Частично изменяет ресурс:

PATCH /api/users/42 HTTP/1.1
Content-Type: application/json

{
  "role": "admin"
}

Сервер изменит только переданное поле.

Формат тела зависит от API. Это может быть обычный JSON-объект или специальный формат патча.

Пример JSON Merge Patch:

PATCH /api/users/42 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "middleName": null
}

В JSON Merge Patch значение null обычно означает удаление свойства.

Пример JSON Patch:

PATCH /api/users/42 HTTP/1.1
Content-Type: application/json-patch+json

[
  {
    "op": "replace",
    "path": "/role",
    "value": "admin"
  }
]

DELETE

Удаляет ресурс:

DELETE /api/users/42 HTTP/1.1
Authorization: Bearer access-token

Возможный ответ без тела:

HTTP/1.1 204 No Content

Сервер также может вернуть удалённый ресурс или описание результата со статусом 200 OK.

OPTIONS

Сообщает о поддерживаемых возможностях ресурса:

OPTIONS /api/users HTTP/1.1

Ответ:

HTTP/1.1 204 No Content
Allow: GET, POST, OPTIONS

Метод OPTIONS также используется браузерами для предварительных CORS-запросов.


Статус-коды HTTP

Статус-код сообщает результат обработки запроса.

Группа Значение
1xx Информационные ответы
2xx Успешное выполнение
3xx Перенаправление или работа с кэшем
4xx Ошибка на стороне клиента
5xx Ошибка на стороне сервера

Успешные ответы 2xx

200 OK

Запрос успешно выполнен:

HTTP/1.1 200 OK
Content-Type: application/json

Часто используется для успешных GET, PUT и PATCH.

201 Created

Ресурс успешно создан:

HTTP/1.1 201 Created
Location: /api/users/43

Обычно возвращается после POST.

202 Accepted

Запрос принят, но обработка ещё не завершена:

HTTP/1.1 202 Accepted
Location: /api/jobs/81

Подходит для длительных асинхронных операций.

Клиент может получать состояние задачи отдельно:

GET /api/jobs/81 HTTP/1.1

204 No Content

Запрос выполнен, но тело ответа отсутствует:

HTTP/1.1 204 No Content

Часто используется после DELETE или обновления, если клиенту не нужны дополнительные данные.

Ответ 204 не должен содержать тело.

Перенаправления и кэш 3xx

301 Moved Permanently

Ресурс навсегда перемещён:

HTTP/1.1 301 Moved Permanently
Location: https://api.example.com/v2/users

302 Found

Временное перенаправление. Исторически клиенты могут менять метод запроса при переходе.

303 See Other

Предлагает получить результат через GET по другому адресу:

HTTP/1.1 303 See Other
Location: /api/jobs/81

307 Temporary Redirect

Временное перенаправление с сохранением исходного HTTP-метода и тела.

308 Permanent Redirect

Постоянное перенаправление с сохранением исходного метода и тела.

304 Not Modified

Ресурс не изменился, клиент может использовать кэшированную копию:

HTTP/1.1 304 Not Modified
ETag: "user-42-v3"

Ответ 304 не содержит обычное тело ресурса.

Ошибки клиента 4xx

400 Bad Request

Запрос имеет неверный формат:

{
  "error": "invalid_request",
  "message": "Некорректный JSON"
}

401 Unauthorized

Аутентификация отсутствует или не прошла:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer

Несмотря на название, статус 401 означает, что клиент не аутентифицирован.

403 Forbidden

Клиент распознан, но у него нет права выполнять операцию:

{
  "error": "forbidden",
  "message": "Недостаточно прав"
}

Различие:

404 Not Found

Ресурс не найден:

{
  "error": "not_found",
  "message": "Пользователь не найден"
}

405 Method Not Allowed

Ресурс существует, но не поддерживает указанный метод:

HTTP/1.1 405 Method Not Allowed
Allow: GET, PATCH, DELETE

409 Conflict

Запрос конфликтует с текущим состоянием системы:

{
  "error": "email_conflict",
  "message": "Пользователь с таким email уже существует"
}

410 Gone

Ресурс существовал, но был удалён без возможности восстановления.

415 Unsupported Media Type

Сервер не поддерживает формат тела:

HTTP/1.1 415 Unsupported Media Type

Например, API ожидает JSON, а клиент отправляет XML.

422 Unprocessable Content

Формат запроса корректен, но данные не проходят проверку:

{
  "error": "validation_failed",
  "fields": {
    "email": "Некорректный адрес",
    "age": "Возраст должен быть не меньше 18"
  }
}

429 Too Many Requests

Клиент превысил ограничение частоты запросов:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

Ошибки сервера 5xx

500 Internal Server Error

На сервере произошла непредвиденная ошибка:

{
  "error": "internal_error",
  "message": "Не удалось обработать запрос"
}

Клиенту не следует передавать внутренний стек вызовов, SQL-запросы, пароли и другие технические детали.

502 Bad Gateway

Промежуточный сервер получил некорректный ответ от другого сервиса.

503 Service Unavailable

Сервис временно недоступен:

HTTP/1.1 503 Service Unavailable
Retry-After: 120

504 Gateway Timeout

Промежуточный сервер не дождался ответа от другого сервиса.


HTTP-заголовки

HTTP-заголовки передают метаданные запроса и ответа.

Имена заголовков регистронезависимы:

Content-Type: application/json
content-type: application/json

Эти записи имеют одинаковое значение, хотя на практике обычно используется формат с заглавными первыми буквами слов.

Content-Type

Указывает формат тела запроса или ответа:

Content-Type: application/json

Для HTML:

Content-Type: text/html; charset=utf-8

Для обычного текста:

Content-Type: text/plain; charset=utf-8

Для формы:

Content-Type: application/x-www-form-urlencoded

Для формы с файлами:

Content-Type: multipart/form-data; boundary=----FormBoundary

Content-Type описывает фактически передаваемое тело.

Accept

Сообщает, какой формат ответа хочет получить клиент:

Accept: application/json

Несколько вариантов:

Accept: application/json, text/plain;q=0.8

Различие:

Authorization

Передаёт данные аутентификации:

Authorization: Bearer access-token

Или:

Authorization: Basic base64-credentials

User-Agent

Описывает клиентское приложение:

User-Agent: ExampleMobileApp/2.4

Браузеры отправляют собственные значения User-Agent.

Location

Указывает адрес созданного ресурса или адрес перенаправления:

Location: /api/users/43

Content-Length

Содержит размер тела в байтах:

Content-Length: 348

В HTTP/2 и HTTP/3 передача данных устроена иначе, поэтому заголовок не всегда обязателен.

Origin

Показывает источник браузерного запроса:

Origin: https://app.example.com

Источник включает:

Referer

Указывает адрес страницы, с которой был отправлен запрос:

Referer: https://app.example.com/profile

Название Referer исторически записано с ошибкой и сохраняется в HTTP в таком виде.

Пользовательские заголовки

API может использовать собственные заголовки:

X-Request-ID: 4f0f74fb-7cb9-4024-80e2-30458500e9fb

Для новых стандартных решений необязательно добавлять префикс X-. Например:

Request-ID: 4f0f74fb-7cb9-4024-80e2-30458500e9fb

Идентификатор запроса помогает сопоставлять обращения клиента с серверными журналами.


REST

REST (Representational State Transfer) — архитектурный стиль построения распределённых систем и HTTP API.

REST не является отдельным сетевым протоколом. API может работать по HTTP, но не быть RESTful, если оно не следует ресурсному подходу и ограничениям REST.

Ключевая идея: сервер предоставляет ресурсы, а клиент взаимодействует с их представлениями.

Например, пользователь — ресурс:

/api/users/42

Его JSON-представление:

{
  "id": 42,
  "name": "Анна",
  "email": "anna@example.com"
}

Один ресурс может иметь разные представления, например JSON или XML. На практике современные веб-API чаще используют JSON.


Принципы REST

Клиент-сервер

Клиентский интерфейс и серверная логика разделены.

Клиент отвечает за отображение и пользовательское взаимодействие, а сервер — за хранение данных, правила доступа и бизнес-логику.

Отсутствие состояния на сервере

Каждый запрос должен содержать всю информацию, необходимую для его обработки.

Например, токен передаётся в каждом защищённом запросе:

GET /api/profile HTTP/1.1
Authorization: Bearer access-token

Сервер не должен зависеть от того, какой запрос клиент отправлял перед этим.

Это ограничение называется stateless. Оно не запрещает хранить данные пользователей, ресурсы, токены или серверные сессии. Оно означает, что каждый запрос должен быть понятен серверу самостоятельно.

Кэшируемость

Ответ должен явно или неявно определять возможность кэширования:

Cache-Control: public, max-age=300

Корректное кэширование уменьшает задержки и нагрузку на сервер.

Единообразный интерфейс

Ресурсы имеют стабильные адреса, а действия выражаются стандартными HTTP-методами:

GET    /api/users
POST   /api/users
GET    /api/users/42
PATCH  /api/users/42
DELETE /api/users/42

Многоуровневая система

Между клиентом и основным сервером могут находиться:

Клиенту не требуется знать внутреннюю структуру системы.

Код по требованию

Сервер при необходимости может передавать клиенту исполняемый код. Это необязательное ограничение REST и редко рассматривается при проектировании JSON API.


Ресурсный подход

Адреса API обычно описывают существительные, а не действия:

Предпочтительно:
GET /api/users
GET /api/users/42
POST /api/users
DELETE /api/users/42
Менее предпочтительно:
GET  /api/getUsers
POST /api/createUser
POST /api/deleteUser

Действие уже выражается HTTP-методом, поэтому его не нужно повторять в адресе.

Коллекции и отдельные ресурсы

Коллекция пользователей:

/api/users

Конкретный пользователь:

/api/users/42

Заказы пользователя:

/api/users/42/orders

Конкретный заказ:

/api/orders/815

Вложенные маршруты полезны для отображения связи, но слишком глубокую вложенность лучше избегать:

Слишком сложно:
/api/companies/7/departments/3/users/42/orders/815/items/2

Более простой вариант:

/api/order-items/2

Именование ресурсов

Обычно используются существительные во множественном числе:

/api/users
/api/orders
/api/products

Следует выбрать единый стиль и применять его во всём API.

Фильтрация

GET /api/products?category=books&available=true HTTP/1.1

Сортировка

GET /api/products?sort=price HTTP/1.1

Обратная сортировка:

GET /api/products?sort=-price HTTP/1.1

Формат сортировки определяется самим API.

Пагинация по номеру страницы

GET /api/users?page=3&limit=20 HTTP/1.1

Ответ:

{
  "items": [
    {
      "id": 41,
      "name": "Анна"
    },
    {
      "id": 42,
      "name": "Иван"
    }
  ],
  "pagination": {
    "page": 3,
    "limit": 20,
    "total": 94,
    "pages": 5
  }
}

Пагинация по курсору

GET /api/messages?limit=20&cursor=eyJpZCI6MTAwfQ HTTP/1.1

Ответ:

{
  "items": [],
  "nextCursor": "eyJpZCI6MTIwfQ",
  "hasMore": true
}

Курсорная пагинация обычно устойчивее при частом добавлении и удалении записей.

Версионирование

Версия в пути:

/api/v1/users

Версия через заголовок:

Accept: application/vnd.example.v2+json

Версия в пути проще и нагляднее, но любой вариант должен применяться последовательно.


Формат ошибок REST API

Ошибки желательно возвращать в едином формате:

{
  "error": {
    "code": "validation_failed",
    "message": "Данные не прошли проверку",
    "details": [
      {
        "field": "email",
        "message": "Некорректный адрес"
      }
    ],
    "requestId": "req-8c72c34e"
  }
}

Полезные поля:

Клиентская программа должна ориентироваться прежде всего на статус HTTP и машинный код, а не на текст сообщения.


JSON

JSON (JavaScript Object Notation) — текстовый формат обмена структурированными данными.

JSON не привязан только к JavaScript. Его поддерживают практически все современные языки программирования.

Пример:

{
  "id": 42,
  "name": "Анна",
  "active": true,
  "roles": ["editor", "author"],
  "address": {
    "city": "Казань",
    "postalCode": "420000"
  },
  "middleName": null
}

Типы данных JSON

JSON поддерживает:

Тип Пример
Объект {"name": "Анна"}
Массив [1, 2, 3]
Строка "текст"
Число 42, 3.14, -10
Логическое значение true, false
Пустое значение null

Объект

Объект содержит пары «ключ — значение»:

{
  "name": "Анна",
  "age": 28
}

Ключи обязательно записываются в двойных кавычках.

Массив

Массив содержит упорядоченный набор значений:

[
  "admin",
  "editor",
  "author"
]

Элементами массива могут быть объекты:

[
  {
    "id": 1,
    "name": "Анна"
  },
  {
    "id": 2,
    "name": "Иван"
  }
]

Строка

В JSON используются двойные кавычки:

{
  "message": "Привет"
}

Одинарные кавычки недопустимы:

Некорректно:
{'message': 'Привет'}

Число

{
  "count": 15,
  "price": 199.99,
  "temperature": -4.5
}

Специальные значения JavaScript NaN и Infinity не являются допустимыми JSON-значениями.

Логическое значение

{
  "active": true,
  "deleted": false
}

null

{
  "middleName": null
}

null означает явное отсутствие значения. Отсутствующее свойство и свойство со значением null могут иметь разную семантику:

{
  "name": "Анна"
}
{
  "name": "Анна",
  "middleName": null
}

API должно документировать это различие.


Ограничения JSON

JSON не поддерживает:

Некорректный JSON:

{
  // Имя пользователя
  name: "Анна",
  "active": true,
}

Корректный вариант:

{
  "name": "Анна",
  "active": true
}

Даты

Дата обычно передаётся строкой в формате ISO 8601:

{
  "createdAt": "2026-09-11T14:30:00Z"
}

Z означает время UTC.

Дата со смещением:

{
  "createdAt": "2026-09-11T17:30:00+03:00"
}

Формат даты должен быть явно описан в документации API.

Большие числа

JavaScript не может без потери точности обрабатывать все целые числа произвольного размера.

Большой идентификатор безопаснее передавать строкой:

{
  "id": "9223372036854775807"
}

Двоичные данные

Бинарные файлы обычно передаются как отдельное содержимое:

Content-Type: application/pdf

Или через multipart/form-data.

Кодирование файла в Base64 внутри JSON возможно, но увеличивает размер данных и обычно менее эффективно:

{
  "fileName": "document.pdf",
  "contentBase64": "JVBERi0xLjQK..."
}

Работа с JSON в JavaScript

Преобразование объекта в JSON

const user = {
  id: 42,
  name: "Анна",
  active: true
};

const json = JSON.stringify(user);

console.log(json);

Результат:

{"id":42,"name":"Анна","active":true}

Форматирование с отступами:

const formattedJson = JSON.stringify(user, null, 2);

Преобразование JSON в объект

const json = '{"id":42,"name":"Анна"}';
const user = JSON.parse(json);

console.log(user.name);

Некорректный JSON вызовет исключение:

try {
  const data = JSON.parse(responseText);
  console.log(data);
} catch (error) {
  console.error("Некорректный JSON", error);
}

Отправка JSON через fetch()

const response = await fetch("/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json"
  },
  body: JSON.stringify({
    name: "Анна",
    email: "anna@example.com"
  })
});

const data = await response.json();

fetch() не считает статусы 400 или 500 сетевой ошибкой. Статус нужно проверять самостоятельно:

const response = await fetch("/api/users/42");

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.message ?? "Ошибка запроса");
}

const user = await response.json();

Ответ 204 No Content нельзя безусловно разбирать через response.json():

if (response.status === 204) {
  return null;
}

return response.json();

CORS

CORS (Cross-Origin Resource Sharing) — механизм браузера, который управляет запросами между разными источниками.

Источник, или origin, состоит из:

протокол + домен + порт

Примеры разных источников:

https://app.example.com
https://api.example.com

Домены отличаются.

http://example.com
https://example.com

Протоколы отличаются.

http://localhost:3000
http://localhost:8080

Порты отличаются.

Браузер применяет политику одинакового источника и ограничивает JavaScript-доступ к ответам другого источника, если сервер явно не разрешил такой доступ.

CORS — браузерный механизм. Он не мешает отправлять запросы из серверного приложения, curl или Postman.


Простой CORS-запрос

Клиент:

const response = await fetch(
  "https://api.example.com/api/users"
);

Запрос:

GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

Сервер разрешает источник:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

После этого браузер разрешит JavaScript-коду прочитать ответ.

Разрешение для любого источника:

Access-Control-Allow-Origin: *

Такой вариант подходит только для действительно публичных данных и несовместим с запросами, использующими учётные данные браузера.


Предварительный CORS-запрос

Для некоторых междоменных запросов браузер сначала отправляет запрос OPTIONS. Он называется preflight request.

Например, клиент хочет отправить JSON с заголовком авторизации:

await fetch("https://api.example.com/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer access-token"
  },
  body: JSON.stringify({
    name: "Анна"
  })
});

Браузер сначала отправит:

OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type

Сервер должен ответить разрешениями:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600

После успешной проверки браузер отправит основной POST.

Основные CORS-заголовки

Заголовок Назначение
Access-Control-Allow-Origin Разрешённый источник
Access-Control-Allow-Methods Разрешённые методы
Access-Control-Allow-Headers Разрешённые заголовки запроса
Access-Control-Allow-Credentials Разрешение передавать учётные данные
Access-Control-Expose-Headers Заголовки ответа, доступные JavaScript
Access-Control-Max-Age Время кэширования preflight-ответа

Запросы с учётными данными

Для отправки cookie клиент должен включить credentials:

const response = await fetch(
  "https://api.example.com/api/profile",
  {
    credentials: "include"
  }
);

Сервер отвечает:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

При использовании учётных данных нельзя отвечать:

Access-Control-Allow-Origin: *

Необходимо указать конкретный разрешённый источник.

Доступ к заголовкам ответа

Не все заголовки междоменных ответов доступны JavaScript. Дополнительные заголовки нужно открыть явно:

Access-Control-Expose-Headers: ETag, X-Request-ID

После этого клиент может прочитать их:

const etag = response.headers.get("ETag");

Частые ошибки CORS

CORS настраивается на сервере, а не исправляется добавлением заголовка в клиентский запрос.

Неверно:

fetch(url, {
  headers: {
    "Access-Control-Allow-Origin": "*"
  }
});

Этот заголовок должен вернуть сервер.

Режим no-cors обычно не решает проблему:

fetch(url, {
  mode: "no-cors"
});

Браузер вернёт непрозрачный ответ, содержимое которого JavaScript не сможет прочитать.

CORS не является:

Сервер всё равно должен проверять аутентификацию, авторизацию и входные данные.


Кэширование HTTP

HTTP-кэш сохраняет ответ и повторно использует его без полной загрузки с сервера.

Кэшировать данные могут:

Кэширование бывает двух основных видов:

  1. использование свежей копии без обращения к серверу;
  2. проверка устаревшей копии через условный запрос.

Cache-Control

Заголовок Cache-Control управляет кэшированием.

max-age

Определяет время свежести ответа в секундах:

Cache-Control: public, max-age=300

Ответ считается свежим 300 секунд.

public

Разрешает хранить ответ в браузерных и общих кэшах:

Cache-Control: public, max-age=3600

Подходит для общедоступных данных.

private

Разрешает хранение только в приватном кэше конкретного клиента:

Cache-Control: private, max-age=60

Подходит для персонализированных ответов, которые нельзя хранить в общем CDN-кэше.

no-cache

Разрешает сохранить ответ, но требует проверки на сервере перед повторным использованием:

Cache-Control: no-cache

Название может вводить в заблуждение: no-cache не запрещает хранение.

no-store

Запрещает хранить ответ:

Cache-Control: no-store

Подходит для чувствительных данных:

Cache-Control: no-store

must-revalidate

Запрещает использовать устаревшую копию без проверки:

Cache-Control: max-age=300, must-revalidate

s-maxage

Определяет срок свежести для общих кэшей, например CDN:

Cache-Control: public, max-age=60, s-maxage=600

Браузер может хранить ответ 60 секунд, а общий кэш — 600 секунд.

immutable

Сообщает, что содержимое ресурса не изменится:

Cache-Control: public, max-age=31536000, immutable

Подходит для файлов с хешем в имени:

/app.a3f91c2.js
/styles.74c812a.css

При изменении содержимого создаётся новое имя файла.

stale-while-revalidate

Разрешает временно использовать устаревший ответ, пока кэш обновляется в фоне:

Cache-Control: public, max-age=60, stale-while-revalidate=300

ETag

ETag — идентификатор версии представления ресурса.

Сервер возвращает:

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "user-42-v3"
Cache-Control: no-cache

{
  "id": 42,
  "name": "Анна"
}

При следующем обращении клиент отправляет условный запрос:

GET /api/users/42 HTTP/1.1
If-None-Match: "user-42-v3"

Если ресурс не изменился:

HTTP/1.1 304 Not Modified
ETag: "user-42-v3"

Тело ресурса повторно не передаётся. Клиент использует сохранённую копию.

Если ресурс изменился, сервер возвращает новый ответ:

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "user-42-v4"

{
  "id": 42,
  "name": "Анна Петрова"
}

Сильные и слабые ETag

Сильный ETag:

ETag: "abc123"

Он означает точное соответствие представления.

Слабый ETag:

ETag: W/"abc123"

Он означает семантическую эквивалентность, даже если отдельные байты представления отличаются.

Предотвращение потери изменений

ETag можно использовать при обновлении ресурса:

PATCH /api/users/42 HTTP/1.1
If-Match: "user-42-v3"
Content-Type: application/json

{
  "name": "Анна Петрова"
}

Сервер выполнит обновление только в том случае, если текущая версия совпадает с "user-42-v3".

Если ресурс уже изменил другой клиент:

HTTP/1.1 412 Precondition Failed

Это помогает избежать перезаписи чужих изменений.


Last-Modified

Сервер может сообщать время последнего изменения:

Last-Modified: Thu, 10 Sep 2026 12:00:00 GMT

Клиент отправляет:

If-Modified-Since: Thu, 10 Sep 2026 12:00:00 GMT

Если ресурс не изменился, сервер возвращает:

HTTP/1.1 304 Not Modified

ETag обычно точнее, поскольку время изменения имеет ограниченную точность и не всегда однозначно описывает версию содержимого.


Vary

Заголовок Vary сообщает кэшу, какие заголовки запроса влияют на ответ:

Vary: Accept-Encoding

Если содержимое зависит от языка:

Vary: Accept-Language

Для динамически разрешаемого CORS-источника может использоваться:

Vary: Origin

Без корректного Vary общий кэш может вернуть вариант ответа, созданный для другого набора заголовков.


Практические стратегии кэширования

Статические файлы с хешем:

Cache-Control: public, max-age=31536000, immutable

Публичный каталог товаров:

Cache-Control: public, max-age=60, s-maxage=300
ETag: "products-v18"

Персональный профиль:

Cache-Control: private, no-cache
ETag: "profile-user-42-v7"

Чувствительные данные:

Cache-Control: no-store

При кэшировании персонализированных ответов важно не допустить их сохранения в общем кэше и выдачи другому пользователю.


Аутентификация в HTTP

Аутентификация определяет, кто выполняет запрос.

Авторизация определяет, какие действия этому субъекту разрешены.

Последовательность:

Аутентификация: «Кто вы?»
Авторизация: «Что вам разрешено?»

HTTP поддерживает разные схемы аутентификации через заголовок Authorization.


Basic Authentication

В Basic Authentication клиент передаёт имя пользователя и пароль:

username:password

Строка кодируется в Base64 и помещается в заголовок:

Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

Base64 — это кодирование, а не шифрование. Исходные данные легко восстановить.

Поэтому Basic Authentication допустимо использовать только через HTTPS.

Запрос:

GET /api/profile HTTP/1.1
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

Если данные отсутствуют или неверны:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Example API"

Пример с curl:

curl \
  --user "username:password" \
  https://api.example.com/api/profile

Basic Authentication часто применяется:

Для публичных пользовательских приложений обычно применяются более гибкие схемы.


Bearer Authentication

Bearer Authentication использует токен доступа:

Authorization: Bearer access-token

Пример:

GET /api/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Accept: application/json

Слово Bearer означает, что доступ получает предъявитель токена. Если токен украден, злоумышленник может использовать его до истечения срока действия или отзыва.

Bearer-токен следует:

Нежелательно:

https://api.example.com/profile?token=secret-token

URL может попасть в историю браузера, журналы сервера, аналитику и заголовок Referer.

Предпочтительно:

Authorization: Bearer secret-token

Bearer и JWT

JWT часто используется как Bearer-токен:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Но Bearer и JWT — не одно и то же:

Bearer-токен может быть обычной случайной строкой, значение которой хранится на сервере.

Ошибки аутентификации

Токен не передан:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer

Токен недействителен:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"

Токен действителен, но прав недостаточно:

HTTP/1.1 403 Forbidden

Безопасность HTTP-аутентификации

HTTPS обязателен для передачи:

Чувствительные данные не следует помещать:

Пример нежелательного URL:

/api/login?username=anna&password=secret

Предпочтительный запрос:

POST /api/login HTTP/1.1
Content-Type: application/json

{
  "username": "anna",
  "password": "secret"
}

Серверные журналы должны скрывать токены и пароли:

Authorization: Bearer [REDACTED]

Документирование API

Документация API должна описывать:


OpenAPI и Swagger

OpenAPI Specification — стандарт машиночитаемого описания HTTP API.

Описание обычно хранится в YAML- или JSON-файле:

openapi.yaml

или:

openapi.json

Swagger — название набора инструментов, работающих с OpenAPI.

К таким инструментам относятся:

OpenAPI — это спецификация, а Swagger — связанная экосистема инструментов.


Основная структура OpenAPI

Минимальный пример:

openapi: 3.1.0

info:
  title: Users API
  version: 1.0.0
  description: API для управления пользователями

servers:
  - url: https://api.example.com

paths:
  /api/users:
    get:
      summary: Получить список пользователей
      responses:
        "200":
          description: Список пользователей

openapi

Версия спецификации:

openapi: 3.1.0

info

Информация об API:

info:
  title: Users API
  version: 1.0.0
  description: API для управления пользователями

servers

Адреса серверов:

servers:
  - url: https://api.example.com
    description: Основной сервер

  - url: https://staging-api.example.com
    description: Тестовый сервер

paths

Маршруты и методы:

paths:
  /api/users:
    get:
      summary: Получить список пользователей

    post:
      summary: Создать пользователя

  /api/users/{userId}:
    get:
      summary: Получить пользователя

Параметры OpenAPI

Параметр пути

paths:
  /api/users/{userId}:
    get:
      summary: Получить пользователя
      parameters:
        - name: userId
          in: path
          required: true
          description: Идентификатор пользователя
          schema:
            type: integer
            minimum: 1

Параметр пути всегда должен иметь:

required: true

Параметр строки запроса

parameters:
  - name: page
    in: query
    required: false
    schema:
      type: integer
      minimum: 1
      default: 1

  - name: limit
    in: query
    required: false
    schema:
      type: integer
      minimum: 1
      maximum: 100
      default: 20

Такое описание соответствует запросу:

GET /api/users?page=2&limit=20 HTTP/1.1

Параметр заголовка

parameters:
  - name: X-Request-ID
    in: header
    required: false
    schema:
      type: string

Тело запроса в OpenAPI

requestBody:
  required: true
  content:
    application/json:
      schema:
        type: object
        required:
          - name
          - email
        properties:
          name:
            type: string
            minLength: 1
          email:
            type: string
            format: email

Пример можно добавить через example:

requestBody:
  required: true
  content:
    application/json:
      schema:
        $ref: "#/components/schemas/CreateUser"
      example:
        name: Анна
        email: anna@example.com

Схемы данных OpenAPI

Повторно используемые схемы размещаются в components:

components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - email
      properties:
        id:
          type: integer
          example: 42
        name:
          type: string
          example: Анна
        email:
          type: string
          format: email
          example: anna@example.com
        active:
          type: boolean
          example: true
        createdAt:
          type: string
          format: date-time
          example: "2026-09-11T14:30:00Z"

Использование схемы через $ref:

schema:
  $ref: "#/components/schemas/User"

Массив пользователей:

schema:
  type: array
  items:
    $ref: "#/components/schemas/User"

Ответы OpenAPI

responses:
  "200":
    description: Пользователь найден
    content:
      application/json:
        schema:
          $ref: "#/components/schemas/User"

  "404":
    description: Пользователь не найден
    content:
      application/json:
        schema:
          $ref: "#/components/schemas/Error"

Ответ без тела:

responses:
  "204":
    description: Пользователь удалён

Схема ошибки:

components:
  schemas:
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: not_found
        message:
          type: string
          example: Пользователь не найден
        requestId:
          type: string
          example: req-8c72c34e

Аутентификация в OpenAPI

Bearer Authentication:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

Применение ко всему API:

security:
  - bearerAuth: []

Применение только к отдельной операции:

paths:
  /api/profile:
    get:
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Профиль пользователя

Basic Authentication:

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic

Полный пример OpenAPI

openapi: 3.1.0

info:
  title: Users API
  version: 1.0.0
  description: REST API для управления пользователями

servers:
  - url: https://api.example.com

tags:
  - name: Users
    description: Операции с пользователями

security:
  - bearerAuth: []

paths:
  /api/users:
    get:
      tags:
        - Users
      summary: Получить список пользователей
      operationId: getUsers
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1

        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20

      responses:
        "200":
          description: Список пользователей
          content:
            application/json:
              schema:
                type: object
                required:
                  - items
                  - pagination
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
                  pagination:
                    $ref: "#/components/schemas/Pagination"

        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      tags:
        - Users
      summary: Создать пользователя
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUser"
            example:
              name: Анна
              email: anna@example.com

      responses:
        "201":
          description: Пользователь создан
          headers:
            Location:
              description: Адрес созданного пользователя
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

        "400":
          description: Некорректный запрос
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "409":
          description: Пользователь уже существует
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "422":
          description: Ошибка проверки данных
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/users/{userId}:
    get:
      tags:
        - Users
      summary: Получить пользователя
      operationId: getUser
      parameters:
        - $ref: "#/components/parameters/UserId"

      responses:
        "200":
          description: Пользователь найден
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

        "404":
          description: Пользователь не найден
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    patch:
      tags:
        - Users
      summary: Частично изменить пользователя
      operationId: updateUser
      parameters:
        - $ref: "#/components/parameters/UserId"

      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateUser"

      responses:
        "200":
          description: Пользователь изменён
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

        "404":
          description: Пользователь не найден
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    delete:
      tags:
        - Users
      summary: Удалить пользователя
      operationId: deleteUser
      parameters:
        - $ref: "#/components/parameters/UserId"

      responses:
        "204":
          description: Пользователь удалён

        "404":
          description: Пользователь не найден
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    UserId:
      name: userId
      in: path
      required: true
      description: Идентификатор пользователя
      schema:
        type: integer
        minimum: 1

  responses:
    Unauthorized:
      description: Требуется аутентификация
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    User:
      type: object
      required:
        - id
        - name
        - email
        - active
        - createdAt
      properties:
        id:
          type: integer
          example: 42
        name:
          type: string
          example: Анна
        email:
          type: string
          format: email
          example: anna@example.com
        active:
          type: boolean
          example: true
        createdAt:
          type: string
          format: date-time
          example: "2026-09-11T14:30:00Z"

    CreateUser:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
          minLength: 1
        email:
          type: string
          format: email

    UpdateUser:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        email:
          type: string
          format: email
        active:
          type: boolean

    Pagination:
      type: object
      required:
        - page
        - limit
        - total
        - pages
      properties:
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20
        total:
          type: integer
          example: 94
        pages:
          type: integer
          example: 5

    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: not_found
        message:
          type: string
          example: Пользователь не найден
        requestId:
          type: string
          example: req-8c72c34e

Практика документирования API

Документация должна соответствовать реальному поведению сервера.

Полезно автоматизировать:

Каждая операция должна иметь уникальный operationId:

operationId: getUser

Названия должны быть стабильными, поскольку генераторы клиентов могут использовать их как имена функций.

Примеры:

getUsers
getUser
createUser
updateUser
deleteUser

Следует описывать не только успешный ответ, но и основные ошибки:

responses:
  "200":
    description: Успешный ответ
  "400":
    description: Некорректный запрос
  "401":
    description: Требуется аутентификация
  "403":
    description: Недостаточно прав
  "404":
    description: Ресурс не найден
  "500":
    description: Внутренняя ошибка

Общий пример REST API

Получение пользователя:

curl \
  --request GET \
  --header "Accept: application/json" \
  --header "Authorization: Bearer access-token" \
  https://api.example.com/api/users/42

Запрос:

GET /api/users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer access-token

Ответ:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-cache
ETag: "user-42-v3"
Vary: Authorization

{
  "id": 42,
  "name": "Анна",
  "email": "anna@example.com",
  "active": true,
  "createdAt": "2026-09-11T14:30:00Z"
}

Повторная проверка кэша:

GET /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer access-token
If-None-Match: "user-42-v3"

Если ресурс не изменился:

HTTP/1.1 304 Not Modified
ETag: "user-42-v3"
Cache-Control: private, no-cache

Частичное обновление:

curl \
  --request PATCH \
  --header "Authorization: Bearer access-token" \
  --header "Content-Type: application/json" \
  --header 'If-Match: "user-42-v3"' \
  --data '{"name":"Анна Петрова"}' \
  https://api.example.com/api/users/42

Запрос:

PATCH /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer access-token
Content-Type: application/json
If-Match: "user-42-v3"

{
  "name": "Анна Петрова"
}

Успешный ответ:

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "user-42-v4"

{
  "id": 42,
  "name": "Анна Петрова",
  "email": "anna@example.com",
  "active": true,
  "createdAt": "2026-09-11T14:30:00Z"
}

Если версия ресурса уже изменилась:

HTTP/1.1 412 Precondition Failed
Content-Type: application/json

{
  "error": {
    "code": "version_conflict",
    "message": "Ресурс был изменён другим клиентом"
  }
}

Краткая памятка

Типичный набор REST-маршрутов:

GET    /api/users       — получить список
POST   /api/users       — создать пользователя
GET    /api/users/42    — получить пользователя
PUT    /api/users/42    — полностью заменить пользователя
PATCH  /api/users/42    — частично изменить пользователя
DELETE /api/users/42    — удалить пользователя

Основные успешные статусы:

200 — запрос выполнен
201 — ресурс создан
202 — запрос принят в обработку
204 — запрос выполнен без тела ответа
304 — ресурс не изменился

Основные ошибки:

400 — некорректный запрос
401 — не выполнена аутентификация
403 — недостаточно прав
404 — ресурс не найден
409 — конфликт состояния
415 — неподдерживаемый формат
422 — ошибка проверки данных
429 — слишком много запросов
500 — внутренняя ошибка сервера
503 — сервис временно недоступен

Основные заголовки:

Content-Type: application/json
Accept: application/json
Authorization: Bearer access-token
Cache-Control: private, no-cache
ETag: "resource-v3"
If-None-Match: "resource-v3"
Location: /api/users/42

Основные правила: