In many backend projects, the DELETE endpoint is often treated as the simplest kind of CRUD operation.
The typical implementation looks like this:
DELETE /users/123
And the logic behind it:
- Find the user
- Delete the data
- Return success
If the data doesn't exist:
404 Not Found
This seems intuitive enough.
But once we dig deeper into REST semantics, idempotency, network retries, and distributed systems, we discover that the DELETE endpoint isn't nearly as simple as it looks.
A well-designed DELETE endpoint shouldn't ask:
"Did I actually perform a delete action?"
It should instead ask:
"After this request completes, is the resource in a nonexistent state?"
This shift in perspective is the key to rethinking DELETE.
1. What Is Idempotency?
In the HTTP specification, idempotent means:
Executing the same request once versus multiple times produces the same final effect on the server's state.
For example:
PUT /users/123
Setting the username:
{
"name": "Tom"
}
- First call:
name = Tom - Second call:
name = Tom
The final state is the same, so PUT is idempotent.
DELETE is expected to be idempotent as well. For example:
Initial state: User 123 exists
| Call | Result |
|---|---|
1st DELETE /users/123
|
User 123 no longer exists |
2nd DELETE /users/123
|
User 123 still doesn't exist |
3rd DELETE /users/123
|
User 123 still doesn't exist |
Even though the first call actually changed the data, subsequent calls don't change anything further — but the final state remains consistent.
So DELETE is naturally idempotent.
2. A Common Mistake: Treating "No Change" as Failure
Many endpoints are designed like this:
First request:
DELETE /users/123
200 OK
{
"success": true
}
Second request:
DELETE /users/123
404 Not Found
{
"success": false,
"message": "User does not exist"
}
This looks reasonable at first glance, but there's a real problem: did the second request actually fail?
The answer is: no.
Because what is the client's actual goal? It's not to ask the server to:
"Perform one delete action"
It's to ensure:
"User 123 does not exist"
After the second request executes, user 123 still doesn't exist — the goal has already been achieved.
So from DELETE's true semantics, the second request should also be considered a success.
3. The Real Semantics of DELETE: Ensuring the Resource Does Not Exist
Many developers think of DELETE as:
"Delete the data"
But a more accurate way to understand it is:
"Ensure the resource enters a nonexistent state"
For example:
DELETE /currencies/123
This expresses:
"Please make sure Currency 123 does not exist."
Therefore:
| Request | Before | After |
|---|---|---|
| 1st | Exists | Does not exist |
| 2nd | Does not exist | Does not exist |
| 3rd | Does not exist | Does not exist |
Final state: does not exist — consistently, every time.
That is what true idempotency means.
4. Why Should "Already Nonexistent" Also Count as Success?
The biggest reason: networks are unreliable.
Consider a real-world scenario:
- Client sends
DELETE - Server deletes successfully
- But the response is lost in transit
- The client doesn't know what happened
- So the client sends
DELETEagain
If the second request returns 404, the client is left confused:
- Did the first request actually succeed?
- Do I need to refresh the page?
- Do I need to show an error to the user?
If DELETE is instead designed so that "already nonexistent" also means success, then:
- First request → deletion succeeds
- Second request → the target state is already satisfied
The client doesn't need any extra logic to handle the ambiguity.
5. What Should success Actually Represent?
This is a critical design principle:
successindicates whether the request completed successfully — not whether the data actually changed.
Many systems run into trouble because they conflate these two concepts.
Wrong approach:
{
"success": false,
"message": "No data was deleted"
}
Because: not deleting any data does not mean the request failed.
Better approach:
{
"success": true,
"data": {
"deleted": false
}
}
Meaning:
- The request executed successfully
- But it didn't produce a data change
These are two separate concepts, and they should be kept separate.
6. Recommended DELETE Response Design
For simple resources
Examples: tags, categories, currencies, personal settings.
Recommended:
DELETE /currencies/123
{
"success": true,
"message": "Deleted successfully"
}
Regardless of whether it's:
- the first deletion,
- a repeated deletion, or
- the resource was already gone,
the response is always success.
When you need to know whether a change actually occurred
{
"success": true,
"data": {
"deleted": true
}
}
- First call:
"deleted": true - Second call:
"deleted": false
Note that success is true in both cases, because the request itself succeeded.
7. Why Are Orders and Contracts Different?
You might ask: shouldn't order deletion work the same way?
Answer: not necessarily.
Orders, contracts, and approval records are no longer simple resources. They represent:
- Business facts
- Legal records
- Financial data
- Audit information
Deletion itself becomes a meaningful business action.
For example, an order typically flows through states:
Created → Paid → Cancelled
And a contract:
Draft → Signed → Terminated
In these scenarios, a real DELETE is usually never performed. Instead, the system records a state change, such as:
status = CANCELLED
or:
deleted = true
deleted_by = xxx
deleted_time = xxx
Because the system needs to know:
- Who deleted it
- Why it was deleted
- When it was deleted
- What state it was in before deletion
8. When Do You Need to Know Whether the Resource Existed Before Deletion?
Sometimes you do. For example:
Audit systems
When an administrator deletes a contract, the system needs to record:
Deleted contract A
Operator: Zhang San
Time: 2026-08-11
Here, whether the resource genuinely existed beforehand matters a great deal.
Financial systems
When deleting a payment record, the system must confirm whether the payment record genuinely existed — otherwise it could cause accounting discrepancies.
But this kind of requirement should not be expressed through success = false. It should instead be handled through:
- Audit logs
- Business status fields
- Dedicated response fields
9. Practical Design Recommendations
For ordinary CRUD resources (users, categories, tags, settings):
-
DELETEmeans "ensure the resource does not exist" - Nonexistence is not a failure
For business objects (orders, contracts, approvals, payments):
- Use state transitions
- Avoid actual deletion
10. Summary
A mature DELETE endpoint design is not this:
Call the delete method
↓
Check whether data was actually deleted
↓
Return success or failure
It should instead be this:
Understand the client's goal
↓
Bring the resource to that target state
↓
Return the outcome of the request
Core idea: the goal of DELETE is not the act of deletion — it's the final state.
Therefore:
- DELETE should be idempotent
- A nonexistent resource is not necessarily a failure
-
successreflects whether the request completed, not whether the data changed - Whether data actually changed is a business-level result
- Business-critical data should not be casually hard-deleted
In one sentence:
Good API design doesn't describe what the server did — it describes what state the client wants to reach.
This is exactly why a seemingly simple DELETE endpoint can reveal so much about a backend engineer's understanding of REST, distributed systems, and business modeling.
Top comments (0)