REST API Design Trade-offs: Bulk Deletes, Request Bodies and Cancellation
Three API questions that came up on a real-time trading platform: should a bulk delete be a DELETE with a body, should one big GET become several small ones, and who cancels a request nobody needs any more?
On this page
Most API design debates are about naming. The interesting ones are about how HTTP behaves between your client and your server, through all the machinery in the middle. Three came up for me in 2026, working on the front end of a real-time trading platform at a fintech, and each one taught me something about the back end.
1. Bulk delete: DELETE with a body, or POST?
The requirement was simple: let a user delete many records at once, quickly. The first design was a DELETE request with
an array of ids in the body. It’s tidy, it reads well, and it worked on a developer’s machine.
The problem is everything between the browser and the server. HTTP semantics say a body on a DELETE has no defined
meaning, and some implementations treat it accordingly:
- Caching proxies, load balancers and security firewalls may strip the body or reject the request.
- Some client libraries and server frameworks don’t support it without overrides.
- Even if it works today, it can fail when infrastructure changes underneath you.
The options, with their trade-offs:
| Design | Pros | Cons |
|---|---|---|
DELETE /items with ids in the body | Reads naturally | Undefined semantics; may be dropped by intermediaries |
DELETE /items?ids=1,2,3 | Valid HTTP | URL length limits; ids end up in access logs |
Many DELETE /items/{id} calls | Purest REST | Slow and chatty for large selections; partial failure |
POST /items/bulk-delete (or /items:batchDelete) with a body | Bodies are well defined on POST; works through any proxy | Less “RESTful”; must be designed as idempotent on purpose |
There was a fair pragmatic counter-argument: the API’s consumers just want it to work, and a server can be configured to
accept bodies on DELETE. That’s true inside a network you control. For an API that might sit behind someone else’s
proxy, I’d choose the POST action endpoint: it’s boring, explicit, and it survives the middle of the internet. Make it
idempotent (deleting an already-deleted id is a success, not an error), return per-item results so partial failures are
visible, and document it as an action rather than pretending it’s a resource.
2. One aggregate GET, or several small ones?
A dashboard fetched one large aggregate response containing several independent datasets. Splitting it into separate requests per panel, which I worked on, had clear benefits:
- Panels load independently, so the fastest data shows first and one slow dataset doesn’t block the screen.
- Caching and refetching get finer-grained. Refreshing one panel doesn’t refetch everything.
- Failures are isolated. One dataset failing shows one error, not a blank page.
The costs are more requests, more connections and more round trips, which HTTP/2 multiplexing largely absorbs. My rule: aggregate when the data is always needed together and changes together; split when panels are independent.
3. Cancellation is part of the contract
When a user changes a filter twice in a second, the first request is now useless. If it’s still in flight, it can arrive after the second one and overwrite fresh data with stale data.
In the browser, AbortController cancels it:
const controller = new AbortController();
const res = await fetch(`/api/positions?account=${id}`, { signal: controller.signal });
// later, when the input changes or the component unmounts:
controller.abort();
Axios accepts the same signal, and React Query passes one to your query function automatically and aborts it when the
query is no longer needed, which is the easiest route if you’re already using it.
The back-end lesson is the important one: aborting a request on the client doesn’t stop the work on the server. The connection closes, but the handler may keep running. For expensive endpoints, the server should notice the client has gone and stop, and anything with side effects must be safe to retry, ideally with an idempotency key, because a cancelled request may still have succeeded.
The common thread
All three are really about the same thing: the network is not a function call (Fallacies of Distributed Computing). Bodies get dropped, requests overlap, connections close half-way. Good API design assumes that and makes the safe path the obvious one.
Related: API Modelling · Rolling Out an API Gateway · Message Delivery Semantics
- #rest
- #api-design
- #http
- #abortcontroller
- #cancellation
- #idempotency
↩ Linked from (4)
Related: WebSockets · Server-Sent Events (SSEs) · Message Delivery Semantics · REST API Design Trade-offs: Bulk Deletes, Request Bodies and Cancellation
…penAPI, typing the data layer, and arguing about pagination, error shapes and bulk operations (API Modelling, REST API Design Trade-offs: Bulk Deletes, Request Bodies and Cancellation). Some real server-side work. Python…
Related: Designing Snapshot and Delta APIs · REST API Design Trade-offs: Bulk Deletes, Request Bodies and Cancellation
- Spring and Java, RefreshedGrowing
…een at the boundary: OpenAPI contracts, error shapes, pagination, and what a request body is allowed to mean (REST API Design Trade-offs: Bulk Deletes, Request Bodies and Cancellation, API Modelling). Money needs BigDeci…
↪ Links to (4)
Related by topic
- Growing
Snapshot and Delta Sync over WebSockets
Real-time UIs - Growing
The Node.js Event Loop, Streams and Backpressure
Backend - Growingcase study
A Resumable CMS Migration Tool
Distributed systems - Seed
Kubernetes
Platform engineering