Стандартный CRUD хорошо работает для одного ресурса за раз. Но бывают три задачи, где нужен отдельный подход: создать сразу сотню записей, дождаться длительной фоновой задачи и показать ошибку на языке пользователя. Разберём каждую.
Массовые операции
Представьте: клиент хочет создать 50 заказов одним запросом. Можно отправить 50 отдельных POST — но это 50 HTTP-запросов, 50 раз накладные расходы, и клиенту придётся ждать каждый из них. Массовая операция (batch) позволяет передать все элементы в одном запросе.
Как выглядит запрос
Endpoint для массовой операции строится по шаблону POST /resources/batch или POST /resources/batch/<действие>:
POST /api/v1/orders/batch
Content-Type: application/json
{
"items": [
{ "productId": "aaa", "quantity": 2 },
{ "productId": "bbb", "quantity": 1 },
{ "productId": "ccc", "quantity": 5 }
]
}
Частичный успех — не ошибка всей операции
Ключевая идея: если один из элементов не прошёл, остальные обрабатываются как обычно. Это называется частичный успех (partial success).
Сервер возвращает 200 OK с результатом по каждому элементу:
{
"results": [
{ "index": 0, "status": "SUCCESS", "orderId": "..." },
{ "index": 1, "status": "ERROR", "error": { "code": "INSUFFICIENT_STOCK", "detail": "Товар bbb отсутствует на складе" } },
{ "index": 2, "status": "SUCCESS", "orderId": "..." }
],
"summary": {
"total": 3,
"succeeded": 2,
"failed": 1
}
}
Что здесь важно:
200 OKдаже при частичной ошибке — это не сбой запроса, а нормальный ответ с результатами.indexпоказывает позицию элемента в исходном массиве (нумерация с нуля).status—SUCCESSилиERRORдля каждого элемента.- При ошибке — объект
errorс кодом и деталью. Это не полный ProblemDetails, потому что ошибка относится к конкретному элементу, а не ко всему запросу. summary— итоговые счётчики: всего, успешно, с ошибкой.
Когда нужна атомарность
Частичный успех — поведение по умолчанию. Иногда нужно противоположное: либо все, либо никто. Это называется атомарность (all-or-nothing). Если сервис поддерживает такой режим, это явно указывается в документации:
«Все элементы обрабатываются в одной транзакции. Ошибка любого элемента приводит к откату всех. При частичном сбое возвращается 400 с индексами упавших элементов.»
Без такой оговорки клиент должен рассчитывать на частичный успех.
Ограничение на размер
Принимать неограниченное количество элементов опасно — это нагрузка на сервер и долгое время ответа. Поэтому максимальный размер массовой операции указывается в документации (например, не более 100 элементов).
Если клиент превысил лимит, сервер возвращает:
HTTP/1.1 400 Bad Request
{
"type": "urn:problem:order-service:batch-size-exceeded",
"status": 400,
"title": "Bad Request",
"detail": "Размер запроса превышает максимум (100 элементов)",
"code": "BATCH_SIZE_EXCEEDED"
}
Асинхронные операции
Некоторые операции не успевают завершиться за время HTTP-запроса. Генерация отчёта за год может занять 30 секунд, массовая рассылка — несколько минут. Держать соединение открытым так долго — плохая идея: сеть может оборваться, у клиента истечёт таймаут.
Решение: сервер сразу принимает задачу, возвращает ответ, а обработку делает в фоне. Клиент периодически проверяет статус — это называется опрос (polling).
Шаг 1: отправить задачу
POST /api/v1/reports/generate
Content-Type: application/json
{ "dateFrom": "2026-01-01", "dateTo": "2026-12-31" }
Сервер отвечает 202 Accepted — запрос принят, но ещё не выполнен:
HTTP/1.1 202 Accepted
Location: /api/v1/tasks/550e8400-...
{
"taskId": "550e8400-...",
"status": "PENDING",
"createdAt": "2026-05-26T10:30:00Z",
"statusUrl": "/api/v1/tasks/550e8400-..."
}
Locationв заголовке — адрес, по которому можно проверять статус.statusUrlв теле — то же самое, для клиентов, которые не читают заголовки ответа.taskId— идентификатор задачи.
Шаг 2: опрашивать статус
Клиент периодически делает GET /api/v1/tasks/{id}. Пока задача выполняется:
{
"taskId": "550e8400-...",
"status": "PROCESSING",
"progress": 45,
"createdAt": "2026-05-26T10:30:00Z"
}
Когда задача завершилась успешно — появляется ссылка на результат:
{
"taskId": "550e8400-...",
"status": "COMPLETED",
"progress": 100,
"createdAt": "2026-05-26T10:30:00Z",
"completedAt": "2026-05-26T10:35:00Z",
"resultUrl": "/api/v1/reports/550e8400-..."
}
Если задача завершилась с ошибкой — приходит описание проблемы:
{
"taskId": "550e8400-...",
"status": "FAILED",
"createdAt": "2026-05-26T10:30:00Z",
"completedAt": "2026-05-26T10:32:00Z",
"error": {
"code": "REPORT_GENERATION_FAILED",
"detail": "Не удалось сформировать отчёт: данные за период отсутствуют"
}
}
Статусы задачи
Задача проходит через четыре состояния:
| Статус | Что означает |
|---|---|
PENDING | создана, ожидает своей очереди |
PROCESSING | выполняется прямо сейчас |
COMPLETED | завершена; resultUrl обязателен |
FAILED | завершена с ошибкой; error обязателен |
Как часто опрашивать — решает клиент. Обычно раз в 1-5 секунд для коротких задач, раз в 30-60 секунд для длинных. Сервер может подсказать интервал через заголовок Retry-After.
Локализация сообщений об ошибках
Пользователи видят сообщения об ошибках — и хотят видеть их на своём языке. Клиент сообщает предпочтительный язык через заголовок Accept-Language:
GET /api/v1/orders/123
Accept-Language: ru
GET /api/v1/orders/123
Accept-Language: en
Если заголовок не указан — сервер использует язык по умолчанию (как правило, русский).
Что именно локализуется
В ответе об ошибке локализуются два поля:
detailв ProblemDetails — человекочитаемое описание ошибки.messageвviolations— описание рядом с конкретным полем формы.
// Accept-Language: ru
{
"code": "ORDER_NOT_FOUND",
"detail": "Заказ не найден"
}
// Accept-Language: en
{
"code": "ORDER_NOT_FOUND",
"detail": "Order not found"
}
Что локализовать нельзя
Некоторые части ответа специально остаются на английском:
code— машинный код ошибки. Клиентский код делаетswitch (error.code)и не должен зависеть от языка пользователя. Правильно:ORDER_EMPTY, неправильно:ЗАКАЗ_ПУСТОЙ.title— стандартное название HTTP-статуса:Bad Request,Not Found. Всегда по-английски.type— URI или URN, технический идентификатор. Всегда на английском.- Имена JSON-полей —
orderId, а неидЗаказа. Структура JSON одна для всех языков.
Причина простая: эти поля используются программным кодом, а не людьми. Локализовать их — значит сломать клиентов, которые на них полагаются.
Коротко
- Массовая операция принимает список
itemsв одном запросе и возвращает200 OKс результатом по каждому элементу. - По умолчанию — частичный успех: ошибка одного элемента не отменяет остальных.
- Атомарность (все или никто) требует явного указания в документации.
- Превышение лимита размера —
400 BATCH_SIZE_EXCEEDED. - Длительная операция возвращает
202 AcceptedсLocationиtaskId; клиент опрашивает статус через GET. - Статусы задачи:
PENDING→PROCESSING→COMPLETED(сresultUrl) илиFAILED(сerror). - Локализуются только
detailиviolations.message— через заголовокAccept-Language. - Коды ошибок, заголовки HTTP, имена JSON-полей остаются на английском.
Что почитать дальше
- Ошибки в REST API: ProblemDetails и коды — как устроены
code,detail,type. - Заголовки запросов и ответов —
Idempotency-Keyдля массовых операций,Locationдля асинхронных. - Ограничения, файлы и версионирование — смежные темы.