-2

I'm really looking for one good example how to PUT operations should be implemented correctly.

What I understood until now:

  • The operation must be idempotent
  • When the resource doesn't exists it will be created new one, API returns status code 201 - Created
  • When the resource exists and has changed, resource will be updated, API returns status code 200 - Ok
  • When the resource data hasn't been changed, no update will happen and API returns status code 204 - No content

So I created reference implementation in .NET Core.

At first I made all fields required because of idempotency.

/// <summary>
///     To-Do model.
/// </summary>
public class ToDoInsertUpdateModel
{
    /// <summary>
    ///     Value indicating whether To-Do is completed.
    /// </summary>
    /// <value><c>true</c> if this To-Do is completed; otherwise, <c>false</c>.</value>
    [Required]
    [DisplayName("Completed")]
    [JsonProperty]
    public bool? IsCompleted { get; set; }

    /// <summary>
    ///     To-Do description.
    /// </summary>
    /// <value>To-Do description.</value>
    [Required]
    [JsonProperty]
    public string Description { get; set; }

    /// <summary>
    ///     Date when To-Do must begin.
    /// </summary>
    /// <value>Start date when To-Do must begin.</value>
    [Required]
    [JsonProperty]
    [DisplayName("Start date")]
    public DateTime? StartDate { get; set; }

    /// <summary>
    ///     Date when To-Do must be finished.
    /// </summary>
    /// <value>Due date when To-Do must be finished.</value>
    [Required]
    [JsonProperty]
    [DisplayName("Due date")]
    public DateTime? DueDate { get; set; }
}

I commented the code for all folks who don't use .Net to be able to understand as my question doesn't targeting any specific platform.

    /// <summary>
    ///     Create new or update existing To-Do.
    /// </summary>
    /// <param name="todoToInsertOrUpdate">To-Do to be updated or created.</param>
    /// <param name="id">To-Do identifier.</param>
    /// <response code="201">To-Do was created. Returns created To-Do.</response>
    /// <response code="200">To-Do was updated. Returns updated To-Do.</response>
    /// <response code="204">To-Do wasn't updated as data hasn't changed.</response>
    /// <response code="500">Returns on unexpected server error.</response>
    [ProducesResponseType(201)]
    [ProducesResponseType(200)]
    [ProducesResponseType(204)]
    [ProducesResponseType(500)]
    [Route("{id:int}")]
    [HttpPut]
    public async Task<ActionResult<ToDoModel>> CreateUpdate(int id,
        [FromBody] ToDoInsertUpdateModel todoToInsertOrUpdate)
    {
        if (!ModelState.IsValid) { return BadRequest(); }

        // Try to get To-Do from database based on id
        ToDoModel foundToDo = await _toDoAppDbContext.ToDos.SingleOrDefaultAsync(toDo => toDo.Id == id);

        if (foundToDo == null) // If To-Do wasn't found than new one will be created
        {
            var toDo = _mapper.Map<ToDoModel>(todoToInsertOrUpdate);
            await _toDoAppDbContext.ToDos.AddAsync(toDo);
            await _toDoAppDbContext.SaveChangesAsync();
            // Return status code 201 with the new created resource in a response body and location in the header
            return CreatedAtAction(nameof(Get), new {id = toDo.Id}, toDo);
        }

        _toDoAppDbContext.Entry(foundToDo).CurrentValues.SetValues(todoToInsertOrUpdate);
        // Get count of updated rows
        int changedRows = await _toDoAppDbContext.SaveChangesAsync();

        // If more than zero rows than resource was updated, this case it returns status code 200,
        // otherwise resource wasn't updated in this case it returns status code 204
        return changedRows != 0 ? foundToDo : (ActionResult<ToDoModel>) NoContent();
    }        

I have two questions:

  • Is the correct what I had understood know about PUT method specification
  • When I call PUT http://api/todos/8 on not existing resource, it will create new one. But because last segment is in this case a primary key generated by database engine, API will return new resource with the different key. Is this correct behavior?

Note: Please don't post any RFC specification snippets as an answer. I really looked into this and I would welcome to have very concrete answer based on best practice or some accepted behavior by community.

  • I don’t have a problem to be downvoted but I would really like to know the reason. – Anton Kalcik Jan 31 at 8:36
  • 1
    This question belongs on a different part of stack exchange: codereview.stackexchange.com – BobDalgleish Jan 31 at 14:03
  • 1
    Well sometimes it is very difficult to choose the correct stack exchange. But in this case I'm not asking about code or code quality. I'm asking more about how to understand the specification and if my assumptions are correct. – Anton Kalcik Jan 31 at 14:25
3

POST to a URL creates a resource that is a child of the resource at the endpoint you're POSTing. For example, a POST to http://api/todos creates a child resource which might have a location of http://api/todos/8. In this way it's important to note that the server is defining the URL.

PUT to a URL creates (if it does not exist) or replaces the resource that exists at the endpoint you're PUTing. For example, a PUT to http://api/todos/8 creates/replaces a resource with the location of http://api/todos/8. In this way it's important to note that the client is defining the URL.

https://tools.ietf.org/html/rfc7231#section-4.3.3

1) You're correct that a PUT must be idempotent.

2) That is not the correct behavior. That behavior looks more like what I'd expect from a POST to http://api/todos per my description above. To reiterate, PUT replaces the resource at a URL known by the client.

Snippet from the RFC:

Proper interpretation of a PUT request presumes that the user agent knows which target resource is desired. A service that selects a proper URI on behalf of the client, after receiving a state-changing request, SHOULD be implemented using the POST method rather than PUT. If the origin server will not make the requested PUT state change to the target resource and instead wishes to have it applied to a different resource, such as when the resource has been moved to a different URI, then the origin server MUST send an appropriate 3xx (Redirection) response; the user agent MAY then make its own decision regarding whether or not to redirect the request.

  • Hi and welcome! Thank you for you solid answer.I know the specification but I don't really understand it that's I asked very specific question. How should API react on PUT api/todos/8 when the resource doesn't exist? What about status code 204? Is it correct? – Anton Kalcik Jan 30 at 22:11
  • @AntonKalcik Because your server is defining the URLs of the resources under api/todos then PUT should never be being used for creation; it will only be used for updates. Based on that you should likely reject the PUT request to api/todos/8 and respond with a 4xx level response code to let the client know that it has made an error. A 204 indicates a successful request which I don't believe is your intent here. I would personally use a 404 since the resource I'm trying to modify does not exist but I could see an argument for a 409 as well in this case. – Poke Jan 30 at 22:28
  • Yeah that's the point lot of people are saying that PUT could be also be for creation, but I'm fine to not do this. Status code 204 is in the case when resource was found but nothing has changed, so it wasn't necessary to update the resource. – Anton Kalcik Jan 30 at 22:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.