HTTP REST API
HTTP (Hypertext Transfer Protocol) — протокол прикладного уровня для обмена данными между клиентом и сервером.
Клиент отправляет HTTP-запрос, а сервер возвращает HTTP-ответ:
Клиент → HTTP-запрос → Сервер
Клиент ← HTTP-ответ ← СерверВ роли клиента могут выступать:
- браузер;
- мобильное приложение;
- сервер другого сервиса;
- консольная программа;
- API-клиент, например Postman;
- JavaScript-код с
fetch().
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-ответа
- HTTP-методы
- Статус-коды HTTP
- HTTP-заголовки
- REST
- Принципы REST
- Ресурсный подход
- Формат ошибок REST API
- JSON
- Типы данных JSON
- Ограничения JSON
- Работа с JSON в JavaScript
- CORS
- Простой CORS-запрос
- Предварительный CORS-запрос
- Кэширование HTTP
- `Cache-Control`
- `ETag`
- `Last-Modified`
- `Vary`
- Практические стратегии кэширования
- Аутентификация в HTTP
- Basic Authentication
- Bearer Authentication
- Безопасность HTTP-аутентификации
- Документирование API
- OpenAPI и Swagger
- Основная структура OpenAPI
- Параметры OpenAPI
- Тело запроса в OpenAPI
- Схемы данных OpenAPI
- Ответы OpenAPI
- Аутентификация в OpenAPI
- Полный пример OpenAPI
- Практика документирования API
- Общий пример REST API
- Краткая памятка
Структура HTTP-запроса
HTTP-запрос состоит из:
- стартовой строки;
- заголовков;
- пустой строки;
- необязательного тела.
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.1POST— HTTP-метод;/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-ответ состоит из:
- строки статуса;
- заголовков;
- пустой строки;
- необязательного тела.
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/43
{
"id": 43,
"name": "Анна",
"email": "anna@example.com"
}Строка статуса:
HTTP/1.1 201 CreatedHTTP/1.1— версия протокола;201— статус-код;Created— текстовое описание статуса.
Заголовок Location указывает адрес созданного ресурса:
Location: /api/users/43HTTP-методы
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.1GET не следует использовать для операций, изменяющих данные:
Нежелательно:
GET /api/users/42/delete
Предпочтительно:
DELETE /api/users/42HEAD
Работает аналогично 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.1204 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/users302 Found
Временное перенаправление. Исторически клиенты могут менять метод запроса при переходе.
303 See Other
Предлагает получить результат через GET по другому адресу:
HTTP/1.1 303 See Other
Location: /api/jobs/81307 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": "Недостаточно прав"
}Различие:
401— необходимо войти или предоставить корректные данные аутентификации;403— пользователь известен, но действие ему запрещено.
404 Not Found
Ресурс не найден:
{
"error": "not_found",
"message": "Пользователь не найден"
}405 Method Not Allowed
Ресурс существует, но не поддерживает указанный метод:
HTTP/1.1 405 Method Not Allowed
Allow: GET, PATCH, DELETE409 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: 120504 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=----FormBoundaryContent-Type описывает фактически передаваемое тело.
Accept
Сообщает, какой формат ответа хочет получить клиент:
Accept: application/jsonНесколько вариантов:
Accept: application/json, text/plain;q=0.8Различие:
Content-Type— формат отправляемого тела;Accept— желаемый формат ответа.
Authorization
Передаёт данные аутентификации:
Authorization: Bearer access-tokenИли:
Authorization: Basic base64-credentialsUser-Agent
Описывает клиентское приложение:
User-Agent: ExampleMobileApp/2.4Браузеры отправляют собственные значения User-Agent.
Location
Указывает адрес созданного ресурса или адрес перенаправления:
Location: /api/users/43Content-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Многоуровневая система
Между клиентом и основным сервером могут находиться:
- прокси;
- балансировщики нагрузки;
- API-шлюзы;
- CDN;
- кэширующие серверы.
Клиенту не требуется знать внутреннюю структуру системы.
Код по требованию
Сервер при необходимости может передавать клиенту исполняемый код. Это необязательное ограничение 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"
}
}Полезные поля:
code— стабильный машинный код ошибки;message— понятное описание;details— дополнительные сведения;field— поле с ошибкой;requestId— идентификатор для поиска в журналах.
Клиентская программа должна ориентироваться прежде всего на статус 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 не поддерживает:
- комментарии;
- функции;
undefined;- даты как отдельный тип;
- двоичные данные как отдельный тип;
- завершающие запятые;
- ключи без двойных кавычек.
Некорректный 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 не является:
- механизмом аутентификации;
- защитой API от серверных клиентов;
- заменой проверки прав;
- защитой от всех межсайтовых атак.
Сервер всё равно должен проверять аутентификацию, авторизацию и входные данные.
Кэширование HTTP
HTTP-кэш сохраняет ответ и повторно использует его без полной загрузки с сервера.
Кэшировать данные могут:
- браузер;
- промежуточный прокси;
- CDN;
- серверный шлюз;
- клиентское приложение.
Кэширование бывает двух основных видов:
- использование свежей копии без обращения к серверу;
- проверка устаревшей копии через условный запрос.
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-storemust-revalidate
Запрещает использовать устаревшую копию без проверки:
Cache-Control: max-age=300, must-revalidates-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=300ETag
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 ModifiedETag обычно точнее, поскольку время изменения имеет ограниченную точность и не всегда однозначно описывает версию содержимого.
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/profileBasic 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;
- не помещать в URL;
- не записывать в открытые журналы;
- ограничивать по сроку действия;
- ограничивать по области разрешений;
- безопасно хранить;
- при необходимости отзывать и обновлять.
Нежелательно:
https://api.example.com/profile?token=secret-tokenURL может попасть в историю браузера, журналы сервера, аналитику и заголовок Referer.
Предпочтительно:
Authorization: Bearer secret-tokenBearer и JWT
JWT часто используется как Bearer-токен:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Но Bearer и JWT — не одно и то же:
- 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 обязателен для передачи:
- паролей;
- Basic credentials;
- Bearer-токенов;
- cookie сессии;
- персональных данных.
Чувствительные данные не следует помещать:
- в URL;
- в строку запроса;
- в сообщения ошибок;
- в открытые логи;
- в публичный клиентский код.
Пример нежелательного 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 должна описывать:
- назначение API;
- базовый URL;
- доступные ресурсы;
- методы и пути;
- параметры пути и строки запроса;
- заголовки;
- тело запроса;
- форматы ответов;
- статус-коды;
- ошибки;
- аутентификацию;
- ограничения частоты запросов;
- пагинацию;
- версионирование;
- примеры.
OpenAPI и Swagger
OpenAPI Specification — стандарт машиночитаемого описания HTTP API.
Описание обычно хранится в YAML- или JSON-файле:
openapi.yamlили:
openapi.jsonSwagger — название набора инструментов, работающих с OpenAPI.
К таким инструментам относятся:
- Swagger UI — интерактивная документация;
- Swagger Editor — редактор спецификации;
- Swagger Codegen — генерация клиентского и серверного кода.
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.0info
Информация об 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
Документация должна соответствовать реальному поведению сервера.
Полезно автоматизировать:
- проверку OpenAPI-файла;
- генерацию интерактивной документации;
- генерацию типов и клиентов;
- контрактное тестирование;
- проверку запросов и ответов;
- выявление несовместимых изменений.
Каждая операция должна иметь уникальный 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Основные правила:
- используйте существительные в адресах ресурсов;
- выражайте действия HTTP-методами;
- возвращайте подходящие статус-коды;
- используйте единый формат ошибок;
- передавайте JSON с
Content-Type: application/json; - проверяйте данные на сервере;
- передавайте пароли и токены только через HTTPS;
- не помещайте токены в URL;
- настраивайте CORS на сервере;
- явно задавайте правила кэширования;
- применяйте
ETagдля проверки версии ресурса; - поддерживайте OpenAPI-описание в соответствии с реальным API.