Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    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?

    Growingtended planted 3 min read
    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:

    DesignProsCons
    DELETE /items with ids in the bodyReads naturallyUndefined semantics; may be dropped by intermediaries
    DELETE /items?ids=1,2,3Valid HTTPURL length limits; ids end up in access logs
    Many DELETE /items/{id} callsPurest RESTSlow and chatty for large selections; partial failure
    POST /items/bulk-delete (or /items:batchDelete) with a bodyBodies are well defined on POST; works through any proxyLess “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

    ↪ Links to (4)