← назад к разделу

Стандартный 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 показывает позицию элемента в исходном массиве (нумерация с нуля).
  • statusSUCCESS или 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.
  • Статусы задачи: PENDINGPROCESSINGCOMPLETEDresultUrl) или FAILEDerror).
  • Локализуются только detail и violations.message — через заголовок Accept-Language.
  • Коды ошибок, заголовки HTTP, имена JSON-полей остаются на английском.

Что почитать дальше

  • Ошибки в REST API: ProblemDetails и коды — как устроены code, detail, type.
  • Заголовки запросов и ответов — Idempotency-Key для массовых операций, Location для асинхронных.
  • Ограничения, файлы и версионирование — смежные темы.