Choosing the Right HTTP Method for REST APIs
8 minute read
One of the most common questions in REST API design is which HTTP method to use for different operations. This guide focuses on the distinctions between POST, PUT, and PATCH—three methods often confused with one another—and provides clear guidelines on when to use each.
HTTP Methods Overview
Method | Idempotent | Safe | Cacheable | Purpose |
---|---|---|---|---|
GET | Yes | Yes | Yes | Retrieve data |
POST | No | No | Rarely | Create resources or trigger processes |
PUT | Yes | No | No | Replace resources entirely |
PATCH | No* | No | No | Partially update resources |
DELETE | Yes | No | No | Remove resources |
*PATCH can be implemented to be idempotent, but isn’t inherently so
When to Use POST?
POST is primarily used to create new resources or trigger processes where the server is responsible for determining the resource identifier. It is not idempotent, meaning that multiple identical POST requests may have different effects.
Key Characteristics of POST
- Not idempotent: Submitting the same POST request multiple times typically creates multiple distinct resources
- Server-assigned identifiers: The server determines the identifier for the new resource
- Typically sent to collection URIs:
/api/tasks
,/api/users
- Can represent actions: May trigger processes beyond simple resource creation
- Not cacheable: POST responses are generally not cached (with rare exceptions)
- Not safe: Modifies state on the server
Common Use Cases for POST
Creating new resources where the server assigns the identifier
POST /api/tasks HTTP/1.1 { "title": "Complete documentation", "dueDate": "2025-06-15" } HTTP/1.1 201 Created Location: /api/tasks/42 { "id": 42, "title": "Complete documentation", "dueDate": "2025-06-15", "created": "2025-05-10T14:30:00Z" }
Creating subordinate resources within a parent resource
POST /api/tasks/42/comments HTTP/1.1 { "text": "This is coming along nicely!" }
Triggering operations that don’t fit the resource model (controller resources)
POST /api/tasks/42/send-reminder HTTP/1.1 { "recipients": ["[email protected]"] }
Submitting form data
POST /api/contact-form HTTP/1.1 { "name": "Jane Doe", "email": "[email protected]", "message": "Great product!" }
Implementation in ASP.NET Core
[HttpPost]
public async Task<IActionResult> CreateTask(TaskCreateDto taskDto)
{
// Input validation
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Map DTO to domain model
var task = _mapper.Map<Task>(taskDto);
// Server generates the ID and any other system properties
task.Id = Guid.NewGuid();
task.Created = DateTime.UtcNow;
task.CreatedBy = User.GetUserId();
// Persist the new resource
await _taskRepository.AddAsync(task);
// Return 201 Created with location header and resource representation
return CreatedAtAction(nameof(GetTask), new { id = task.Id }, task);
}
When to Use PUT?
PUT is used to create or replace a resource at a specific URI. The defining characteristic of PUT is that it is idempotent. This means that making the same PUT request once or multiple times has exactly the same effect (no side effects).
Key Characteristics of PUT
- Idempotent: Multiple identical requests have the same effect as a single request
- Client-assigned identifiers: The client specifies the exact URI where the resource should be placed
- Typically sent to specific resource URIs:
/api/tasks/42
,/api/users/john-doe
- Complete replacement: The entire resource is replaced with the new representation
- Not cacheable: PUT responses are generally not cached
- Not safe: Modifies state on the server
Common Use Cases for PUT
Creating resources with client-specified identifiers
PUT /api/configurations/global-settings HTTP/1.1 { "maintenanceMode": false, "maxLoginAttempts": 5, "sessionTimeout": 30 } HTTP/1.1 201 Created { "maintenanceMode": false, "maxLoginAttempts": 5, "sessionTimeout": 30, "lastModified": "2025-05-10T15:20:00Z" }
Replacing an existing resource completely
PUT /api/tasks/42 HTTP/1.1 { "title": "Update documentation", "dueDate": "2025-07-01", "assignee": "jane", "priority": "high" } HTTP/1.1 200 OK { "id": 42, "title": "Update documentation", "dueDate": "2025-07-01", "assignee": "jane", "priority": "high", "lastModified": "2025-05-10T15:30:00Z" }
Uploading a file to a specific location
PUT /api/documents/annual-report.pdf HTTP/1.1 Content-Type: application/pdf [Binary PDF data]
Implementation in ASP.NET Core
[HttpPut("{id}")]
public async Task<IActionResult> UpdateTask(string id, TaskUpdateDto taskDto)
{
// Input validation
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Check if the resource exists
var existingTask = await _taskRepository.GetByIdAsync(id);
bool isCreating = existingTask == null;
if (isCreating)
{
// Create new resource with client-specified ID
var newTask = _mapper.Map<Task>(taskDto);
newTask.Id = id;
newTask.Created = DateTime.UtcNow;
newTask.CreatedBy = User.GetUserId();
await _taskRepository.AddAsync(newTask);
return CreatedAtAction(nameof(GetTask), new { id }, newTask);
}
else
{
// Full replacement of existing resource
// Note: We completely replace all properties; any properties not provided are set to default values
_mapper.Map(taskDto, existingTask);
existingTask.LastModified = DateTime.UtcNow;
existingTask.LastModifiedBy = User.GetUserId();
await _taskRepository.UpdateAsync(existingTask);
return Ok(existingTask);
}
}
Important Considerations for PUT
Missing Fields: If the request doesn’t include a field that exists on the current resource, the field should be removed or set to a default value. PUT replaces the entire resource.
Conditional Requests: Consider supporting ETags and conditional requests to prevent lost updates in concurrent scenarios:
[HttpPut("{id}")]
[ETag]
public async Task<IActionResult> UpdateTask(string id, TaskUpdateDto taskDto)
{
var existingTask = await _taskRepository.GetByIdAsync(id);
if (existingTask == null)
{
// Create new resource logic...
}
// Check ETag to prevent lost updates
string currentEtag = EntityTagHeaderValue.Parse(
Request.Headers["If-Match"]).Tag.ToString();
string actualEtag = EtagGenerator.Generate(existingTask);
if (currentEtag != actualEtag)
{
return StatusCode(412, "Precondition Failed - Resource has been modified");
}
// Update logic...
}
When to Use PATCH?
PATCH is designed for partial updates to resources. Unlike PUT, which replaces an entire resource, PATCH only modifies the specific fields included in the request.
Key Characteristics of PATCH
- Not inherently idempotent: May have different effects when applied multiple times
- Partial updates: Only modifies specified fields, leaving others unchanged
- Sent to specific resource URIs:
/api/tasks/42
,/api/users/john-doe
- Not cacheable: PATCH responses are generally not cached
- Not safe: Modifies state on the server
- Complex semantics: May require special handling for arrays and nested objects
Common Use Cases for PATCH
Updating specific fields of a resource
PATCH /api/tasks/42 HTTP/1.1 Content-Type: application/json { "status": "in-progress", "priority": "high" } HTTP/1.1 200 OK { "id": 42, "title": "Update documentation", "dueDate": "2025-07-01", "status": "in-progress", "priority": "high", "assignee": "jane", "lastModified": "2025-05-10T16:00:00Z" }
Making operations on fields without knowing current value
PATCH /api/counters/visits HTTP/1.1 Content-Type: application/json-patch+json [ { "op": "increment", "path": "/count", "value": 1 } ]
Complex updates with JSON Patch
PATCH /api/tasks/42 HTTP/1.1 Content-Type: application/json-patch+json [ { "op": "replace", "path": "/status", "value": "in-progress" }, { "op": "add", "path": "/tags/-", "value": "documentation" }, { "op": "remove", "path": "/temporaryFlag" } ]
Implementation in ASP.NET Core
Simple Property Update
[HttpPatch("{id}")]
public async Task<IActionResult> PatchTask(string id, JsonPatchDocument<TaskUpdateDto> patchDoc)
{
// Check if the resource exists
var existingTask = await _taskRepository.GetByIdAsync(id);
if (existingTask == null)
{
return NotFound();
}
// Convert domain model to DTO for patching
var taskDto = _mapper.Map<TaskUpdateDto>(existingTask);
// Apply patch operations to the DTO
patchDoc.ApplyTo(taskDto, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Validate the patched entity
if (!TryValidateModel(taskDto))
{
return BadRequest(ModelState);
}
// Map the updated DTO back to the domain model
_mapper.Map(taskDto, existingTask);
existingTask.LastModified = DateTime.UtcNow;
// Save changes
await _taskRepository.UpdateAsync(existingTask);
return Ok(existingTask);
}
Using JSON Merge Patch
[HttpPatch("{id}")]
[Consumes("application/merge-patch+json")]
public async Task<IActionResult> MergePatchTask(string id, [FromBody] JsonElement patchDoc)
{
var existingTask = await _taskRepository.GetByIdAsync(id);
if (existingTask == null)
{
return NotFound();
}
// Convert to JsonDocument
var existingJson = JsonSerializer.SerializeToDocument(existingTask);
// Apply merge patch
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var updatedJson = JsonMergePatch.Apply(existingJson, patchDoc, options);
// Deserialize back to domain model
var updatedTask = JsonSerializer.Deserialize<Task>(updatedJson, options);
updatedTask.LastModified = DateTime.UtcNow;
// Save changes
await _taskRepository.UpdateAsync(updatedTask);
return Ok(updatedTask);
}
Method Selection Decision Tree
Is the operation safe (read-only)?
├─► Yes → Use GET
│ ↓
├─► No → Is the client specifying the exact URI of the resource?
│ ├─► Yes → Is it a complete replacement?
│ │ ├─► Yes → Use PUT
│ │ └─► No → Use PATCH
│ └─► No → Is it a resource creation?
│ ├─► Yes → Use POST
│ └─► No → Is it an action that doesn't fit CRUD?
│ ├─► Yes → Use POST
│ └─► No → Consider custom method or restructuring
Comparison of POST, PUT, and PATCH
Aspect | POST | PUT | PATCH |
---|---|---|---|
URI Target | Collection or controller resource | Specific resource URI | Specific resource URI |
Idempotency | No | Yes | Not inherently |
Resource ID | Server-generated | Client-specified | Existing resource only |
Effect on Resource | Creates new resource | Replaces entire resource | Updates specific fields |
Request Payload | Complete new resource | Complete resource representation | Only changed fields or operations |
Missing Fields | Server assigns defaults | Removes or sets to defaults | Unchanged |
Success Status Code | 201 Created | 200 OK or 201 Created | 200 OK |
Cacheable | Rarely | No | No |
Safe | No | No | No |
Best Practices
Follow Semantic Meaning: Use each method according to its defined semantic meaning in the HTTP specification.
Use Correct Status Codes:
- POST: 201 Created (with Location header) for resource creation
- PUT: 200 OK for updates, 201 Created for creation
- PATCH: 200 OK or 204 No Content
Be Consistent: Apply methods consistently across all API endpoints.
Document Clearly: Ensure your API documentation clearly explains the behavior of each endpoint.
Consider Idempotency: Design endpoints to be idempotent when possible to improve reliability.
Support Conditional Requests: Use ETags and If-Match headers for concurrency control.
Handle Partial Updates Carefully: Be explicit about how PATCH operations work in your API.
Conclusion
- Use POST for creating resources where the server assigns the identifier or for non-CRUD operations
- Use PUT for creating or replacing resources at a specific URI known by the client
- Use PATCH for partial updates to existing resources when you don’t want to replace the entire resource
- Use GET for retrieving resources
- Use DELETE for removing resources
By using HTTP methods according to their intended purposes, you create a more intuitive, maintainable, and standards-compliant API that behaves as clients expect.
Additional Resources
- RFC 7231 - HTTP/1.1 Semantics: POST, GET, PUT, DELETE
- RFC 5789 - PATCH Method for HTTP
- RFC 6902 - JavaScript Object Notation (JSON) Patch
- RFC 7396 - JSON Merge Patch
- Microsoft REST API Guidelines