Most explanations of HTTP verbs in ASP.NET Core Web API stop at the dictionary definition: GET reads, POST creates, PUT updates, DELETE removes. That's true, but it doesn't explain why real enterprise APIs often bend these rules — and understanding why matters more in practice than reciting the definitions. This article walks through HTTP verb selection using a real HR/Payroll workflow scenario: submitting, updating, and removing leave requests.
The Textbook Mapping
In a typical Web API controller, the four core verbs map to CRUD operations like this:
csharp
[ApiController]
[Route("api/[controller]")]
public class LeaveRequestController : ControllerBase
{
[HttpGet("get-emp-details")]
public IActionResult GetEmployeeDetails(int finSlno, string empId, int createdBy)
{
// fetch and return data
return Ok(employeeDetails);
}
[HttpPost("save-leave-request")]
public IActionResult SaveLeaveRequest([FromBody] LeaveRequestDto request)
{
// create a new record
return Ok(result);
}
[HttpPut("update-leave-request")]
public IActionResult UpdateLeaveRequest([FromBody] LeaveRequestDto request)
{
// update an existing record
return Ok(result);
}
[HttpDelete("delete-leave-request/{id}")]
public IActionResult DeleteLeaveRequest(int id)
{
// remove a record
return Ok(result);
}
}
This is correct, and it's what most tutorials teach. But real enterprise systems — particularly HRMS and Payroll applications — frequently deviate from this pattern for reasons that only make sense once you understand what's actually happening to the data underneath.
Why "Delete" Often Isn't Really DELETE
Consider a leave request that an employee wants to withdraw. The textbook answer says: call [HttpDelete], remove the row from the database, done.
In practice, most HR and Payroll systems don't want a "delete" action to genuinely erase the record. There are real, practical reasons for this:
Audit trails — organizations often need to prove what happened and when, especially for anything touching attendance, payroll, or approvals. Permanently deleting a leave request destroys that history.
Compliance — payroll systems in particular are subject to regulations that may require retaining records for a defined period, regardless of whether the user considers them "deleted."
Traceability during disputes — if an employee claims a leave request was withdrawn incorrectly, having the original record (even if marked inactive) is the only way to investigate what actually happened.
Because of this, many systems implement what's called a soft delete: instead of removing the row, the operation updates a status field:
csharp
public class LeaveRequest
{
public int LeaveRequestId;
public string EmpID;
public int Status; // 0 = Pending, 1 = Approved, 2 = Withdrawn
}
A "delete" action becomes, in reality, an update to Status:
csharp
[HttpPost("save-delete-role-mapping")]
public IActionResult SoftDeleteLeaveRequest([FromBody] List requests)
{
foreach (var req in requests)
{
req.Status = 2; // mark as withdrawn, don't remove the row
}
// save changes
return Ok(result);
}
Because this is technically an update operation, not a true deletion, using HttpPost instead of [HttpDelete] is a defensible, common real-world choice — not a mistake or a shortcut. The verb should reflect what actually happens to the data, not just what the button on the screen says.
Why POST Sometimes Wins Even for Genuine Deletes
There's a second, more practical reason POST shows up in place of DELETE in real systems: payload complexity.
[HttpDelete] is traditionally designed around simple identifiers, often passed in the URL:
csharp
[HttpDelete("delete-leave-request/{id}")]
public IActionResult Delete(int id) { ... }
This works cleanly for deleting one record by one ID. But real operations are frequently batch operations — deleting or withdrawing multiple leave requests at once, each potentially needing additional context (who's performing the action, why, which workflow step it affects):
csharp
[HttpPost("save-delete-role-mapping")]
public IActionResult BatchDelete([FromBody] List requests) { ... }
Passing a list of complex objects in a DELETE request body is technically possible but goes against how DELETE has traditionally been used and supported across tooling. POST, which has always supported rich request bodies, handles this case more naturally — which is exactly why it's common to see "delete" operations implemented as POST endpoints in real enterprise codebases.
A Practical Rule of Thumb
None of this means HTTP verb conventions don't matter — they're still a useful default. The practical guidance is:
Use GET for anything that only reads data and has no side effects
Use POST for creating new records, and also for operations — including some deletes — that involve complex payloads or don't map cleanly to a single identifier
Use PUT for straightforward updates to an existing, fully-identified record
Use DELETE for simple, single-identifier removals where a true, permanent delete is actually intended
When you encounter an API that doesn't follow the textbook mapping exactly — like a "delete" endpoint implemented as POST — it's worth asking why, rather than assuming it's wrong. Often, as in the soft-delete case above, there's a real business reason: the data isn't actually being removed, it's being marked, and the verb reflects that reality rather than the UI label.
Takeaway
HTTP verb selection in a real Web API isn't just about matching a CRUD acronym to a decorator. It's about accurately representing what actually happens to the underlying data. A "delete" that's really a status update should probably be a POST or PUT, not a DELETE — and recognizing that distinction is a sign of understanding the system, not a shortcut around convention.
Top comments (0)