Sometimes, we need to update an existing resource. This resource might be a single document or might be a document in a repository of type collection. For example, to update a product in a shop.
These are simple updates that will simply replace the overall resource with the new one. No additional checking will be done.
This is an unsafe and idempotent operation. PUT
suits well here. The request will be done against the identifier of the resource to be updated, for example, /products/12
. The body will contain the new resource that will override the previous one, along with an entity header specifying the representation being used, for example application/json
.
Example:
PUT /products/12 HTTP/1.1
Content-Type: application/json
{
'name': 'Personal Computer',
'price': 550
}
If a read-only field conflicts with the expected value, it may be ignored, or a 409 - Conflict
status code can be returned.
For this, a mutation is used. In this case, as in a PUT
operation in REST, a full replacement will be done. So, the mutation will receive as an argument the new state of the resource, as in the following definition:
type Mutation {
updateProduct(id: ID!, product:ProductInput!): Product
}
That can be used like this:
mutation UpdateFullProduct($product:ProductInput!) {
updateProduct(product: $product) {
id
price
}
}
Variables:
{
"product": {
"id": 5,
"name": "Smartphone",
"price": 350
}
}
Note that the operation has been named UpdateFullProduct
; an operation might contain several mutations, so this is a convenient way to wrap them in a structure similar to a function. Also note that the mutations within are run in series. We can define variables to invoke this operation.
Also note that since our mutation updateProduct
returns a Product
, we can ask for the specific fields we are interested in, as we do with queries.
A full update can be carried out using a regular RPC, as in:
service Blog {
rpc UpdateArticle(UpdateArticleRequest)
returns (Article);
}
message UpdateArticleRequest {
Article article = 1;
}
The updated resource will be returned.
Even though it is possible to perform this full update, Google Cloud API Design Guide recommends that a partial update should be used instead, to make the API more resilient to schema evolution.
Here our backend will be protected against race conditions.
Since REST promotes visibility, a RESTful Web Services takes advantage of the HTTP built-in caching and of the Conditional Requests (RFC 7232). Every response might contain:
ETag
- orentity tag
. Part of the HTTP specification, this is a header to represent a specific version of a resource from. Typically, hash functions are used for this. Clients may save a copy of the resource so that, once they are expired (which is controlled by theExpires
and/orCache-Control
headers), they can make a new request sending itsETag
in theIf-None-Match
header field. If the server detects theETag
has not change, then it will return a304 - Not Modified
response.Last-Modified
- This works likeETag
but, unlike this, it is timestamp-based. This timestamp is set intoIf-Modified-Since
header when sending a new request.
When running a safe request, as in GET
, this is useful to save resources. For unsafe requests, like POST
, PUT
, PATCH
or DELETE
, these values can be used to provide concurrency control.
To protect a resource against concurrency problems, like race conditions, we can require any of these headers:
If-Match
forETag
. The operation will take place as long as the currentETag
matches the provided one.If-Unmodified-Since
forLast-Modified
. The operation will be carried out only if currentLast-Modified
value is no higher than the provided inIf-Unmodified-Since
.
When the conditions are not satisfied, a 412 - Precondition Failed
is returned:
- If no conditional headers have been provided:
- If the resource exist:
403 Forbidden
- If the resource does not exist
- If this is a store:
201 Created
- If this is a collection:
404 Not Found
- If this is a store:
- If the resource exist:
- If conditional headers are provided:
- If preconditions match:
200 Ok
- If preconditions do not match:
412 Precondition Failed
- If preconditions match:
Example:
PUT /products/12 HTTP/1.1
Content-Type: application/json
If-Match: W/"2f-1enSYy6fyIcEanN2CM5rqcZISwc"
{
'name': 'Personal Computer',
'price': 550
}
We can also use one-time URIs to implement conditional POST
requests. These are URIs tailored for a specific operation and for a given resource version. Let's suppose we have a comment
resource which includes a link to remove it. This link would be conditional, i.e. it works as long as the given resource has not been modified). To go about this, we generate a one-time URI: this is, a URI which somehow identifies current request, as in:
Link: <http://www.example.com/comments/gtlrx8et2l>;rel="remove"
Unlike REST, neither GraphQL nor gRPC provide a native pattern to prevent concurrent requests. However, it's straight forward to implement the same protection as long as our entities contain some kind of versioning field, like a version number or a last-modified field. This fits well with update operations, but might be cumbersome with patch or delete operations.
The source code comes with a battery of preexisting articles.
curl -v http://localhost:4000/articles
Pick any id
. To update it, code will require you to provide conditional headers. If we were to request an existing article not providing the If-Unmodified-Since
header, a 403 Forbidden
header would be returned:
curl -v --header "Content-Type: application/json" \
--request PUT \
--data '{"title": "This is an updated article", "description": "Description of an updated article"' \
http://localhost:4000/articles/5fa96503bd00b971bafa81d3
When If-Unmodified-Since
is provided, but the preconditions are not met, a 412 Precondition Failed
will be returned:
curl -v --header "Content-Type: application/json" \
--header "If-Unmodified-Since: Sun, 24 Oct 2020 18:31:40 GMT" \
--request PUT \
--data '{"title": "This is an updated article", "description": "Description of an updated article"' \
http://localhost:4000/articles/5fa96503bd00b971bafa81d3
Finally, if the preconditions are met, the resource is successfully updated:
curl -v --header "Content-Type: application/json" \
--header "If-Unmodified-Since: Sun, 25 Oct 2020 18:31:40 GMT" \
--request PUT \
--data '{"title": "This is an updated article", "description": "Description of an updated article"' \
http://localhost:4000/articles/5fa96503bd00b971bafa81d3
To update an article in our example, we can run this operation:
mutation UpdateArticle($id:ID!, $article:ArticleInput!) {
updateArticle(id:$id,article:$article) {
title
description
}
}
With these variables:
{
"id": "5faeed572bbb8d6e218829c7",
"article": {
"title": "New title",
"description": "New description"
}
}
The rpc
to create a new article is as follows:
service Blog {
rpc UpdateArticle(UpdateArticleRequest)
returns (Article);
}
message UpdateArticleRequest {
Article article = 1;
}
We can update an existing article using the client application, npm run grpcc
, and then:
client.updateArticle({article:{id: "5fc3ffe378b3dd2565ed83f3", title: "Updated title", description:"Updated description"}}, pr)
The updated article will be returned.