Choosing the Right HTTP Method for REST APIs

Detailed comparison of HTTP methods POST, PUT and PATCH and their proper usage in REST APIs

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

MethodIdempotentSafeCacheablePurpose
GETYesYesYesRetrieve data
POSTNoNoRarelyCreate resources or trigger processes
PUTYesNoNoReplace resources entirely
PATCHNo*NoNoPartially update resources
DELETEYesNoNoRemove 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

  1. 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"
    }
    
  2. Creating subordinate resources within a parent resource

    POST /api/tasks/42/comments HTTP/1.1
    {
      "text": "This is coming along nicely!"
    }
    
  3. Triggering operations that don’t fit the resource model (controller resources)

    POST /api/tasks/42/send-reminder HTTP/1.1
    {
      "recipients": ["[email protected]"]
    }
    
  4. 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

  1. 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"
    }
    
  2. 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"
    }
    
  3. 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

  1. 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.

  2. 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

  1. 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"
    }
    
  2. 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 }
    ]
    
  3. 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

AspectPOSTPUTPATCH
URI TargetCollection or controller resourceSpecific resource URISpecific resource URI
IdempotencyNoYesNot inherently
Resource IDServer-generatedClient-specifiedExisting resource only
Effect on ResourceCreates new resourceReplaces entire resourceUpdates specific fields
Request PayloadComplete new resourceComplete resource representationOnly changed fields or operations
Missing FieldsServer assigns defaultsRemoves or sets to defaultsUnchanged
Success Status Code201 Created200 OK or 201 Created200 OK
CacheableRarelyNoNo
SafeNoNoNo

Best Practices

  1. Follow Semantic Meaning: Use each method according to its defined semantic meaning in the HTTP specification.

  2. 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
  3. Be Consistent: Apply methods consistently across all API endpoints.

  4. Document Clearly: Ensure your API documentation clearly explains the behavior of each endpoint.

  5. Consider Idempotency: Design endpoints to be idempotent when possible to improve reliability.

  6. Support Conditional Requests: Use ETags and If-Match headers for concurrency control.

  7. 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