What decisions does an API contract have to make about HTTP DELETE — repeated deletes, soft deletes, cascades, and whether the request may carry a body?
answer
- Idempotent = end state, not the response code
- Repeat → 204 or 404 (or 410) — pick one
- Soft delete ≠ erasure; hide from GET and lists
- Dependents: cascade / 409 restrict / orphan
- No defined semantics for a DELETE body
basics
~20 sDecide what DELETE returns when the resource is already gone (204 or 404 — pick one), whether it soft-deletes or truly removes, what happens to dependent resources, and whether it is asynchronous. Avoid a request body: DELETE bodies have no defined meaning and intermediaries may drop them.
solid answer
~60 sDELETE is idempotent by definition — the *end state* after one call equals the end state after five — so the contract must say what the second call answers. Two defensible choices: `204 No Content` (the client's goal holds) or `404 Not Found` (honest about current state). Pick one and apply it everywhere, because clients build retry logic on it. Other decisions: - **Soft vs hard.** If DELETE only flags a record, say so: it stops appearing in collections, `GET` on it returns 404 or 410, and "deleted" is not the same as "erased" for privacy requests. - **Dependents.** Cascade, refuse with `409` while children exist, or orphan them. All three are valid; silence is not. - **Async.** Large cascades or erasure workflows return `202` plus a job resource. - **Body.** HTTP defines no semantics for a DELETE payload and some proxies and clients strip it, so put parameters in the URL or use POST for a delete-with-options. Also decide who may delete, and consider `If-Match` so a stale client cannot delete a resource it has not seen.
go deeper
Know that DELETE removes a resource, usually answers 204, and is idempotent in terms of the resulting state.
Discuss the repeat-call response choice, soft versus hard delete, and why DELETE bodies are unreliable.
Cover cascade policy, async deletion with 202, If-Match for safe destructive writes, uniqueness constraints under soft delete, and audit.
Standardize the delete contract across services, including erasure workflows for privacy obligations and how tombstones interact with retention policy.
## Idempotent, but what does the second call say? DELETE is idempotent: after it succeeds, the resource is gone, and repeating the request does not make it more gone. That is a statement about *state*, not about the response, and this is where interviews probe. - **`204 No Content` on the repeat** — the client's intent ("ensure it is gone") is satisfied. Friendly to retries: a client that timed out and resends gets a clean success. - **`404 Not Found` on the repeat** — accurate about the present: there is nothing at this URL. More informative for debugging, slightly hostile to naive retry logic. - **`410 Gone`** — it existed and was intentionally removed, permanently. Useful for public URLs where you want crawlers to drop the link, at the cost of remembering deleted ids. All three are used in production. The failure is inconsistency: `204` from one endpoint and `404` from another forces every client to special-case per resource. ## Soft delete Most systems do not actually remove rows. `DELETE /orders/42` sets `deleted_at`. That is fine, but it is a contract decision with consequences the API must express: - The resource disappears from collection listings — unless the contract offers `?includeDeleted=true` for admins. - `GET /orders/42` afterwards should return `404` (or `410`) to ordinary callers; leaking a tombstone with all its fields defeats the point. - "Deleted" is **not** "erased". A privacy erasure request needs a genuinely different operation, and conflating the two is how teams end up believing data is gone when it is not. - Unique constraints get interesting: if `email` is unique and a soft-deleted row still occupies the value, re-registration breaks. Either scope uniqueness to non-deleted rows or accept the constraint. - Undelete, if offered, is its own endpoint (`POST /orders/42/restore`), not a PATCH resurrecting a field. ## Dependents Deleting a parent with children needs an explicit policy: 1. **Cascade** — delete children too. Convenient, dangerous; document exactly what disappears, and consider making a large cascade asynchronous with `202`. 2. **Restrict** — refuse while dependents exist, `409 Conflict` with a body naming what is blocking ("3 active subscriptions"). Safest for destructive operations. 3. **Orphan / detach** — null the reference, keep the children. Valid when children are independently meaningful. Some APIs expose the choice (`DELETE /projects/42?cascade=true`), which is reasonable if the default is the safe one. ## Bodies on DELETE HTTP allows a payload on DELETE but defines no semantics for it, and the practical consequences are real: some proxies strip it, some client libraries cannot send one, some servers ignore it. So do not require one. If you need parameters, use query parameters (`?cascade=true`), headers (`If-Match`), or — for a genuinely complex bulk delete — `POST /orders/bulk-delete` with a body, accepting that you have traded the method's semantics for expressiveness. ## Concurrency and safety `If-Match: "etag"` on DELETE means "delete this only if it is still the version I saw", answering `412` otherwise. This prevents a client from deleting a resource that someone else has just modified — genuinely useful for destructive operations where the user's decision was based on a stale view. For high-value deletions, consider requiring a confirmation token or a two-step flow rather than relying on the client's UI to ask. ## Status codes for the successful case - `204 No Content` — done, nothing to say. The common default. - `200 OK` with a body — done, and here is a summary (useful for cascades: "deleted 1 project, 12 tasks"). - `202 Accepted` — queued; return a job resource. Correct for large cascades, object-store cleanups and erasure workflows, and much better than holding a connection open for 40 seconds. ## Authorization and audit Delete permission is usually distinct from update permission, and deletions are the events auditors care about most. Record who deleted what and when — which is another quiet argument for soft delete, since a tombstone row carries that record naturally. ## Summary of the contract A complete DELETE contract answers five questions: what does success return; what does a repeat return; is it soft or hard; what happens to dependents; and is it synchronous. Answer them once, platform-wide, and the clients get much simpler.
- Is DELETE still idempotent if the second call returns 404?Yes. Idempotency is a property of the resulting state, not of the response code: after the first successful delete the resource is gone, and further calls do not change that. Returning 404 on the repeat is simply reporting the current state honestly. Some APIs prefer 204 because it is friendlier to blind retries, but both preserve idempotency.
- How should a DELETE behave when the resource still has dependent records?Choose an explicit policy and document it: cascade and delete the children, refuse with 409 and name what is blocking, or detach the children by nulling the reference. For destructive or large cascades, refusing by default and offering an explicit opt-in flag is safest, and a big cascade is better returned as 202 with a job resource than performed inside the request.
saying these in an interview costs you the question
- Claiming DELETE is not idempotent because the second call returns 404
- Requiring a request body on DELETE
- Soft-deleting while still returning the resource from GET and collection endpoints
- Treating soft delete as satisfying a data-erasure obligation
- Cascading deletes silently, with no documentation of what else disappears