Standard CRUD works well for one resource at a time. But there are three tasks that need a separate approach: create a hundred records at once, wait for a long-running background job, and show an error in the user's language. Let's cover each.
Batch operations
Imagine a client wants to create 50 orders in a single request. You could send 50 separate POSTs — but that's 50 HTTP requests, 50 rounds of overhead, and the client has to wait for each one. A batch operation lets you pass all items in one request.
What the request looks like
A batch endpoint follows the pattern POST /resources/batch or POST /resources/batch/<action>:
POST /api/v1/orders/batch
Content-Type: application/json
{
"items": [
{ "productId": "aaa", "quantity": 2 },
{ "productId": "bbb", "quantity": 1 },
{ "productId": "ccc", "quantity": 5 }
]
}
Partial success is not a failure of the whole operation
The key idea: if one of the items fails, the rest are processed as usual. This is called partial success.
The server returns 200 OK with a per-item result:
{
"results": [
{ "index": 0, "status": "SUCCESS", "orderId": "..." },
{ "index": 1, "status": "ERROR", "error": { "code": "INSUFFICIENT_STOCK", "detail": "Product bbb is out of stock" } },
{ "index": 2, "status": "SUCCESS", "orderId": "..." }
],
"summary": {
"total": 3,
"succeeded": 2,
"failed": 1
}
}
What matters here:
200 OKeven on a partial failure — this is not a request failure, but a normal response with results.indexshows the item's position in the original array (zero-based).status—SUCCESSorERRORfor each item.- On error — an
errorobject with a code and a detail. This is not a full ProblemDetails, because the error concerns a specific item rather than the whole request. summary— the final counters: total, succeeded, failed.
When you need atomicity
Partial success is the default behavior. Sometimes you need the opposite: all or nothing. This is called atomicity (all-or-nothing). If a service supports this mode, it's stated explicitly in the documentation:
"All items are processed in a single transaction. A failure of any item rolls back all of them. On a partial failure, a 400 is returned with the indexes of the failed items."
Without such a note, the client should expect partial success.
Size limit
Accepting an unlimited number of items is dangerous — it's a load on the server and a long response time. That's why the maximum batch size is stated in the documentation (for example, no more than 100 items).
If the client exceeds the limit, the server returns:
HTTP/1.1 400 Bad Request
{
"type": "urn:problem:order-service:batch-size-exceeded",
"status": 400,
"title": "Bad Request",
"detail": "Request size exceeds the maximum (100 items)",
"code": "BATCH_SIZE_EXCEEDED"
}
Async operations
Some operations can't finish within the time of an HTTP request. Generating a yearly report may take 30 seconds, a bulk mailing — several minutes. Holding the connection open that long is a bad idea: the network may drop, and the client's timeout will expire.
The solution: the server accepts the task right away, returns a response, and does the processing in the background. The client checks the status periodically — this is called polling.
Step 1: submit the task
POST /api/v1/reports/generate
Content-Type: application/json
{ "dateFrom": "2026-01-01", "dateTo": "2026-12-31" }
The server replies 202 Accepted — the request has been accepted but not yet completed:
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-..."
}
Locationin the header — the address where the status can be checked.statusUrlin the body — the same thing, for clients that don't read response headers.taskId— the task identifier.
Step 2: poll the status
The client periodically does GET /api/v1/tasks/{id}. While the task is running:
{
"taskId": "550e8400-...",
"status": "PROCESSING",
"progress": 45,
"createdAt": "2026-05-26T10:30:00Z"
}
When the task has completed successfully, a link to the result appears:
{
"taskId": "550e8400-...",
"status": "COMPLETED",
"progress": 100,
"createdAt": "2026-05-26T10:30:00Z",
"completedAt": "2026-05-26T10:35:00Z",
"resultUrl": "/api/v1/reports/550e8400-..."
}
If the task failed, a description of the problem arrives:
{
"taskId": "550e8400-...",
"status": "FAILED",
"createdAt": "2026-05-26T10:30:00Z",
"completedAt": "2026-05-26T10:32:00Z",
"error": {
"code": "REPORT_GENERATION_FAILED",
"detail": "Failed to build the report: no data for the period"
}
}
Task statuses
A task goes through four states:
| Status | What it means |
|---|---|
PENDING | created, waiting in the queue |
PROCESSING | running right now |
COMPLETED | finished; resultUrl is required |
FAILED | finished with an error; error is required |
How often to poll is up to the client. Usually once every 1-5 seconds for short tasks, once every 30-60 seconds for long ones. The server can suggest an interval via the Retry-After header.
Localizing error messages
Users see error messages — and they want to see them in their own language. The client indicates its preferred language via the Accept-Language header:
GET /api/v1/orders/123
Accept-Language: ru
GET /api/v1/orders/123
Accept-Language: en
If the header is not provided, the server uses the default language (typically Russian).
What exactly gets localized
Two fields in an error response get localized:
detailin ProblemDetails — the human-readable description of the error.messageinviolations— the description next to a specific form field.
// Accept-Language: ru
{
"code": "ORDER_NOT_FOUND",
"detail": "Заказ не найден"
}
// Accept-Language: en
{
"code": "ORDER_NOT_FOUND",
"detail": "Order not found"
}
What must not be localized
Some parts of the response deliberately stay in English:
code— the machine error code. Client code doesswitch (error.code)and must not depend on the user's language. Correct:ORDER_EMPTY; wrong:ЗАКАЗ_ПУСТОЙ.title— the standard HTTP status name:Bad Request,Not Found. Always in English.type— a URI or URN, a technical identifier. Always in English.- JSON field names —
orderId, notидЗаказа. The JSON structure is the same for all languages.
The reason is simple: these fields are used by program code, not by people. Localizing them means breaking the clients that rely on them.
In short
- A batch operation accepts a list of
itemsin one request and returns200 OKwith a per-item result. - The default is partial success: one item's failure doesn't cancel the rest.
- Atomicity (all or nothing) requires an explicit note in the documentation.
- Exceeding the size limit —
400 BATCH_SIZE_EXCEEDED. - A long-running operation returns
202 AcceptedwithLocationandtaskId; the client polls the status via GET. - Task statuses:
PENDING→PROCESSING→COMPLETED(withresultUrl) orFAILED(witherror). - Only
detailandviolations.messageare localized — via theAccept-Languageheader. - Error codes, HTTP headers, and JSON field names stay in English.
What to read next
- Errors in REST API: ProblemDetails and codes — how
code,detail, andtypeare structured. - Request and response headers —
Idempotency-Keyfor batch operations,Locationfor async ones. - Limits, files, and versioning — adjacent topics.