You need an HTTP API endpoint that creates or updates several hundred records in one request. How would you shape the URL, the method and the payload, and what limits would you put in the contract?
answer
- POST to a dedicated /batch or :batchCreate URL
- envelope + array, never a bare array
- client correlation id per item, not array index
- publish max items, max body size, ordering = none
- rate-limit by item count; 413 on oversize, never truncate
basics
~20 sPOST to a dedicated collection-level URL such as /orders:batchCreate or /orders/batch with an array of items, each carrying a client-supplied id so results can be correlated. Publish a hard item limit, a body size limit, and a documented failure mode.
solid answer
~50 s**Method**: POST. A batch is not idempotent by default and does not replace a single resource, so PUT and PATCH are the wrong fit. **URL**: a distinct endpoint, not the plain collection - `POST /orders/batch` or `POST /orders:batchCreate`. Overloading `POST /orders` to accept either an object or an array makes the response shape ambiguous and breaks the 201 + `Location` contract for the single-create case. **Payload**: an envelope with an array, not a bare array, so you have somewhere to put options later: `{"items":[...], "atomic":false}`. Every item carries a **client-supplied correlation id**, because array position is a fragile way to match results when the server reorders or omits. **Contract limits**: a documented maximum item count (typically 100-1000), a maximum body size, and a stated ordering guarantee (usually none). Say explicitly whether the batch is atomic. Add a request-level idempotency mechanism if callers will retry. And size the limit so the worst-case batch finishes inside your gateway timeout.
code
http · 10 linesPOST /v1/orders:batchCreate HTTP/1.1
Content-Type: application/json
{
"atomic": false,
"items": [
{ "clientRef": "a1", "sku": "X-100", "qty": 2 },
{ "clientRef": "a2", "sku": "X-200", "qty": 1 }
]
}go deeper
Say POST to a dedicated batch URL with an array of items and note that there should be a size limit.
Justify POST over PUT/PATCH, insist on an envelope plus correlation ids, and name the concrete limits you would publish.
Add operational framing: item-based rate limiting, gateway timeout budget as the driver of the size cap, and how retries are handled for a non-idempotent POST.
Weigh homogeneous versus heterogeneous batch designs against the cost of re-implementing routing, authorisation and observability per sub-request, and consider whether async job submission is the better contract.
## Why a batch endpoint at all Per-record HTTP requests cost a round trip, a TLS record, gateway policy evaluation and a database transaction each. For bulk import, sync jobs and mobile offline flush, that overhead dominates. A batch endpoint amortises it. The design question is how to express "many operations" in a protocol whose request/response pair is built around one resource. ## Method Use **POST**. PUT means "replace the resource at this URL with this representation", which is false for a batch. PATCH means "apply this modification to the resource at this URL", which is closer but still names a single target. POST is the protocol's designated method for "process this payload according to the resource's own semantics", and it carries no idempotency promise - matching the honest default for a batch. ## URL Give the batch its **own endpoint**. Two conventions are common: a sub-path (`POST /orders/batch`, `POST /batch`) or a custom-method suffix (`POST /orders:batchCreate`, popularised by Google's API guidelines). Either is fine; consistency matters more. What to avoid is content-sniffing the plain collection endpoint - accepting either `{...}` or `[{...},{...}]` at `POST /orders`. The response contract then has to differ by request shape: a single create returns 201 with a `Location` header, a batch cannot. Clients, SDK generators and OpenAPI schemas all handle this badly. ## Payload shape Wrap the array in an envelope: - **Room to grow.** Options such as `atomic`, `continueOnError` or `dryRun` need somewhere to live. Adding them to a bare top-level array is a breaking change. - **Correlation ids.** Each item should carry a caller-chosen id (`clientRef`, `requestId`). Matching results by array index breaks the moment the server filters, reorders or drops items, and it makes partial-failure reports painful to read in logs. - **Homogeneous vs heterogeneous.** Most batches are homogeneous - many creates of one type - which keeps validation and schemas simple. Heterogeneous batches, where each item names its own method and path (the style used by Facebook's Graph API batch endpoint and by OData `$batch`), are far more powerful and far more expensive: you are re-implementing a request router, and authorisation, rate limiting and observability all have to be re-applied per sub-request rather than at the edge. Only take that on when callers genuinely need mixed operations in one round trip. ## Limits belong in the contract A batch endpoint without limits is a denial-of-service surface and a timeout generator. Publish and enforce: - **Max items per request** - pick a number whose worst-case processing time fits comfortably inside your gateway and load-balancer timeouts. 100-1000 is the usual band. - **Max body size** - enforced before parsing, so a hostile 500 MB body is rejected at the edge. - **Rate limiting counted in items, not requests** - otherwise one caller sending 1000-item batches consumes 1000x the quota of a caller sending singles, for the same nominal request rate. - **Ordering guarantee** - state plainly that items may be processed in any order and possibly in parallel, unless you commit otherwise. Callers will assume sequential execution if you are silent. - **Failure semantics** - atomic or best-effort, stated in the docs and ideally selectable per request. Reject oversized batches with **413 Content Too Large** (formerly Payload Too Large) or 400 with a clear error, rather than silently truncating - silent truncation is the cruellest possible failure because the caller sees success. ## Retries Because the endpoint is POST, a client that times out cannot know whether the batch applied. Either make items naturally idempotent through a caller-supplied key, or support a request-level idempotency mechanism so a repeated batch is deduplicated rather than reapplied.
- Why not just accept an array at POST /orders instead of adding a second endpoint?Because the single-create contract is 201 with a `Location` header pointing at the new resource, and a batch has no single location. You would need the response shape and status to vary by request shape, which breaks generated clients and OpenAPI descriptions. A separate URL keeps each contract unambiguous.
- How should rate limiting treat a batch request?Count items, not requests. Otherwise a caller sending 1000-item batches at the same request rate as a caller sending singles consumes a thousand times the backend work for the same quota. Charge the quota per item, and reject over-limit batches at the edge before parsing the body.
saying these in an interview costs you the question
- Using PUT or PATCH for a batch because 'it updates things'
- Accepting a bare top-level JSON array, leaving nowhere to add options
- Correlating results by array position instead of a client-supplied id
- Having no maximum item count, or silently truncating oversized batches
- Rate limiting by request count so batches bypass quotas