How a RESTful API server reacts to requests
Learn how to properly design RESTful APIs communication with clients, accounting for request structure, authentication, and caching.
This series of articles shows you how to derive an easy-to-use, robust, efficient API to serve users on the web or on mobile devices. We are using the principles of RESTful architecture over HTTP. In the first piece, we started from a list of specs for a simple bike rental service, defining URLs and the HTTP methods to serve the app. In this second part, we will talk in more detail about how the server should react to incoming requests with status codes. We will also talk about how to identify who is the user performing a request (authentication), why Cross-Origin Resource Sharing (CORS) matters for APIs, how caching can improve performance, and how HTTP optimistic locking can prevent inconsistencies in resources.
Here is where we left off from the previous post: we have the URLs (nouns) and the HTTP methods (actions) our API responds to. Each combination of URL and HTTP method corresponds to a functionality available in the bike rental app:
GET /stations/ | User sees the list of stations |
GET /rents/ | User sees the history of rents |
POST /rents/ | User rents a bike |
PUT /rents/{id}/ | User changes destination station |
DELETE /rents/{id}/ | User cancels the active rent |
For these URLs + methods, we already defined some basic structure of the responses to valid requests. However, requests can be wrong or can fail for various reasons. We need to answer questions like:
- How can the API tell clients the request worked as expected?
- How can the API say the request was wrong?
- Depending on the error the API returned, what clients can do to fix the request?
Status Codes: reporting what the request did
While navigating in your browser, you have probably stumbled upon error messages like “500 – Server Error” or “404 – Resource Not Found”. Those numbers are called HTTP status codes. Each of them has a well-defined meaning to help client applications (in this case, your browser) and developers deal with error situations, as well as indicating when things went well. The status code is part of the HTTP response, as you can see in the first line of this response example:
HTTP/1.1 200 OK
Connection: keep-alive
Date: Sat, 15 Jul 2017 13:31:25 GMT
Content-Type: application/json
Content-Length: 29
{
"text": "Hello world!"
}
In the previous article, we said that one of the qualities of RESTful APIs is simplicity. One of the ways to achieve this is to enforce the use of a uniform interface. Among other things, this means making good use of conventions and protocols. Part of this is properly choosing the right HTTP status codes to return in responses depending on different contexts. We’ll learn how apply this to our bike rental application.
Understanding HTTP status codes
There are dozens of status codes, but don’t worry—you don’t need to know them all off the top of your head. HTTP status codes are divided into classes grouped by the first digit of the number. The available classes are:
- 1xx – Informational responses
- 2xx – Success
- 3xx – Redirection
- 4xx – Client errors
- 5xx – Server errors
Some status codes should contain additional information in the body or in specific headers of the response. Thus, when defining the a status code, in addition to selecting the one with the most appropriate meaning, be sure to check the additional requirements. Let’s see what each of those classes are and which are their most common status codes. We’ll ignore the 1xx class because it’s not used much.
2xx – Success class
These status codes are returned when the server succeeded in processing the request. In the previous article, we defined only the happy-path responses of the API, i.e., responses for correct requests. This means all responses we saw had status codes from this class. Commonly used status codes here are 200 OK for regular successful requests with a response body, 201 Created for successful requests that created some resource, and 204 No Content for successful requests without a response body.
The links in the previous paragraph point to the httpstatuses.com website, a good reference for standardized status codes descriptions. If you need more details about each status code usage, including requirements on how responses should be formed, check those links.
3xx – Redirection class
These are used when the server found the requested resource somewhere else. To find the resource, the client should make another request to the new URL. In APIs, it’s common to return 301 Moved Permanently for resources that moved to another URL. There’s an important status code in this class that’s not related to redirection: 304 Not Modified. We’ll discuss it in the Caching section of this article.
4xx – Client error class
These are used when the server received a wrong request. This means the client structured the request incorrectly. We always have to anticipate that clients can do something wrong (or even act maliciously). That’s the nature of client-server apps. Because the client doesn’t hold all server state, the client can make wrong requests for various reasons:
- Unauthenticated user.
- Bad user input that can’t be validated client-side, like a wrong password.
- Bad user input that the client-side didn’t validate.
- Out-of-sync errors, like updating a resource that is already deleted.
- Users trying to access resources without permission.
- etc.
Common status code here are 400 Bad Request for requests with invalid or malformed data, 401 Unauthorized for requests with wrong authentication credentials, 403 Forbidden for requests where authenticated users have insufficient permission to access the resource, and 404 Not Found for requests to unavailable or hidden resources (more on that later).
As an implementation note, keep track of recurrent client errors. Maybe it’s your server that’s leading clients to do invalid requests. Maybe it’s a bug in your client-side app. Maybe your API documentation is not clear.
5xx – Server error class
When something wrong happens on the server, not because of client request data, but because of server state itself, the error codes will be 5xx. Those should be unanticipated errors, like a broken database connection. Your API should never throw 5xx errors because of wrongdoings by clients. Leave 5xx errors for exceptional and unpredicted cases. Monitor your API responses for 5xx errors, because this usually means your code isn’t properly handling exceptions while processing requests. Or worse: your server machine is facing problems. Commonly, APIs return 500 Internal Server Error when those things happen.
Great! Now that we know what the various status codes mean, let’s see how to use them in our API.
Defining success conditions
Since we already defined URLs and methods for our API, we can now discuss what success means for each URL/method combination. It’s important to properly define success conditions to allow API clients to communicate success in their interfaces to their end users. Ever interacted with a web or mobile app that didn’t properly inform you whether the operation you had just made worked or not? It’s quite awful, forcing you to find another way to check, like refreshing the page, closing and reopening the app, or even contacting support! Even worse, there are apps out there that don’t show any error, but actually didn’t save your changes. This is probably because their APIs don’t properly inform them of the success conditions, or return a 200 OK even for wrong inputs.
So let’s properly define the success conditions for our API. It’s obviously not 200 OK for all that’s wrong or right. Not even 200 OK for all that’s right. There are multiple 2xx status codes, so let’s use each one properly.
GET /stations/ (User sees the list of stations)
A GET over /stations/ always returns something. Even if all stations were inactive, we could return an empty list of stations. The response here always include a body, therefore success status code is a 200 OK. It’s what we’ve already seen in the previous article:
HTTP/1.1 200 OK
Content-Type: application/json
[{
"id": 1,
"name": "John St",
"location": [40.7, -74],
"available_bikes_quantity": 10
},
{
"id": 2,
"name": "Brooklyn Bridge",
"location": [40.7, -73],
"available_bikes_quantity": 2
}]
GET /rents/ (User sees the history of rents)
Similar to the stations, a list is always returned here, empty or not. If the user ever made some rent, a list representation of those rents is returned. If not, an empty list is returned. Since there is always a body, it’s 200 OK here too. An example if the user doesn’t have rents:
HTTP/1.1 200 OK
Content-Type: application/json
[]
POST /rents/ (User rents a bike)
This is an operation to create rents. For successful creations, we can use the 201 Created status code. Ideally, 201 responses include the Location header with the created rent URL, making it easy for the client to access the recently created resource. This is useful from a UX perspective, because it allows the client app to show the details of the created resource by accessing its assigned URL. Example:
REQUEST:
POST /rents/ HTTP/1.1
Content-Type: application/json
{
"origin_station_id": 1,
"bike_number": 10,
"destination_station_id": 2
}
RESPONSE:
HTTP/1.1 201 Created
Location: /rents/321914/
PUT /rents/{id}/ (User changes destination station)
When everything is alright with the PUT, the response is 200 OK. It could also be 204 No Content, but it’s 200 OK if your API returns the updated resource representation after the PUT, which is a good practice. Example:
REQUEST:
POST /rents/321914/ HTTP/1.1
Content-Type: application/json
{
"destination_station_id": 3
}
RESPONSE:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 321914,
"origin_station_id": 1,
"bike_number": 10,
"destination_station_id": 3,
"start_date": "2017-06-08T19:30:39+00:00",
"end_date": null,
"is_active": true
}
DELETE /rents/{id}/ (User cancels the active rent)
When a DELETE succeeds, no content needs to be returned in the response body; after all, we’ve just deleted the resource. All we need to know here is that the cancellation worked. 204 No Content is the most suited here. Example:
HTTP/1.1 204 No Content
Defining error conditions
Under what conditions can our bike rental API operations fail? Status codes here will be always 4xx, because these are errors we can anticipate. As we’ve seen in the description of the 4xx class, anticipated errors include bad input, access to nonexistent resources, out-of-sync state, unauthenticated users, etc. Those are generic problems that can happen in any API. In the Bike rental API, those problems manifest in many ways, some due to wrong user input, others due to the nature of the application domain of renting a bike. Let’s pass through each URL/method combination to analyse the possible failures that can happen:
GET /stations/ (User sees the list of stations)
This always succeeds, even if the user is unauthenticated, because there’s no problem to leave the available stations data public. Also, available stations don’t vary according to user. No anticipated errors here.
GET /rents/ (User sees the history of rents)
This request responds the current user history of rents. It makes sense only if the request is authenticated. Otherwise, what “current user” would mean, if there was no authenticated user? Therefore, a possible error here is 401 Unauthorized. This request can’t succeed without authentication.
POST /rents/ (User rents a bike)
Similar to the previous case, this request fails with 401 Unauthorized if the request isn’t authenticated. You must rent as some user, so you need authentication. But can rent creation fail in other ways? Yes! The client can try to rent from a nonexistent station, rent a nonexistent bike number, or rent a bike that someone else just rented. All those situations are responded to with a 400 Bad Request. It’s good to explain the failure reason in the response body, so the API client can consume this and display to the end user. Here’s an example response for a user who tried to rent a already rented bike:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"errors": {
"bike_number": ["This bike is already rented"]
}
}
PUT /rents/{id}/ (User changes destination station)
This request also makes sense only for authenticated users, so it can fail with 401 Unauthorized. However, it isn’t sufficient to be authenticated to make this request succeed. Even if you’re authenticated, you can change only your own rents. Allowing one user to change rents from another is a security hole! If a user tries to PUT a rent that belongs to another user, the API should return 404 Not Found. It’s more secure than 403 Forbidden because it avoids the disclosure of existing rent ids from other users. If we returned Forbidden instead of Not Found, an attacker would know there’s some rent with that ID, and might be able to do some mischief with that information. In APIs, there’s no advantage in disclosing which resources belong to which users, so let’s “pretend” the requested resource doesn’t exist with a 404, as it doesn’t belong to the requesting user. We’ll talk more about this in the Authentication section. Intuitively, 404 is the appropriate status code if the rent ID in the URL is invalid or nonexistent, too.
There’s an additional kind of error caused by a requirement of the bike rental service: the spec says the destination station can be changed only during the first 10 minutes of the rent. If a client misses that time and tries to change the rent, a 400 Bad Request should be returned. This same status code suits when the user tries to change the destination station to one that doesn’t exist. Those two kinds of 400s differ from each other on the response body, where the failure reason is determined. Here is an example for the case of a user who tried to change the destination station after 10 minutes:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"errors": {
"destination_station_id": [
"Can't change the destination station after 10 minutes of the rent"
]
}
}
DELETE /rents/{id}/ (User cancels the active rent)
You can cancel your active rent only when authenticated, so 401 Unauthorized is a possible failure here. But you can also pass an ID that is invalid, is nonexistent, or belongs to a past reservation or to another user. For those cases, we can return 404 Not Found.
Great! We defined the expected success and error conditions, along with the status codes to represent them. We followed HTTP status code meanings and applied them to our cases. This is the RESTful approach to HTTP APIs: properly using protocol semantics to simplify the API and decouple it from the implementation.
What if the client side never gets the response in the first place due to a connection drop for example? For these cases, REST helps with well-defined semantics for fault tolerance, which RESTful API designers and implementers need to consider.
Defining fault tolerance
What should a client do when it never gets a response? In that case, the client doesn’t know the current server state, so is it fine to just try again? The answer for the these questions rely on two well-defined concepts: safety and idempotency.
In the context of HTTP, safety means that a given request will not produce any changes in the server state. GET is the main safe method in HTTP. Correct implementations of GET requests, as in our bike rental API, don’t have side effects. Secondary effects like logging or throttling are fine, but in general the server shouldn’t change any resources after handling a GET request. Therefore, it’s perfectly safe to retry GET requests if the client somehow lost track of the server state.
On the other hand, idempotency means the resource state doesn’t change after subsequent requests, i.e., a first request can change something, but the following requests will change nothing. This means the request can be replayed any number of times without introducing new changes in the resource. DELETE is the easiest method to understand when we think about idempotency. The first time you make a DELETE /rents/123/
request, you change the rent state by cancelling it. But subsequent DELETE requests to the same /rents/123/
resource won’t change it, since it was already cancelled1. It’s like multiplying by 0. Everything that’s safe is also idempotent, but opposite isn’t true.
A counterexample of safety and idempotency is the POST method for creating resources. If you make repeated POST requests to the same URL, you’ll create multiple resources. You change the server state after every request, so there’s no safety nor idempotency here. If the client connection with the API server is interrupted in the middle of a POST request, it’s always good to check the server state with a GET before making another POST.
Those semantics of safety and idempotency are part of the Uniform Interface of REST. The reason it’s so important is that your API is a User Interface, where the user is the developer. The more familiar people are with an interface, the easier is for them to use it. This also means that you are helping people a lot if you correctly follow protocol rules, as well as stick to conventions. So make sure your application respects the concepts of safety and idempotency in order to leave your users happier.
Also, remember to document those behaviors! Tell your clients which methods are safe or idempotents over which URLs. As an exercise, make a table with all the HTTP methods and URLs we have in our Bike API and try to identify which ones are safe or idempotent. Check the answer in the footnote2.
Enough on error conditions and fault tolerance. We’ve talked a lot about 401 Unauthorized errors, but we haven’t defined how the authentication works in our Bike rental API. Let’s work on this.
Authentication
The main question to answer about Authentication in our API is: how? There are many ways to authenticate a user in an RESTful HTTP API. We will introduce the most popular stateless authentication methods to help you chose the one that best suits your application. The stateless concept is important for a truly RESTful Authentication, because REST demands stateless communication between client and server, i.e., “each request from client to server must contain all of the information necessary to understand the request.”3 If you have server-side sessions holding state about the user, you’re not doing stateless authentication. Commonly, cookie-based authentication methods hold state information. They are not RESTful.4
It’s important to note that, regardless of your choice, you should not roll your own authentication protocol. A secure authentication scheme requires expert knowledge in cryptography algorithms. Also, there are tons of edge cases that can pass unnoticed by an inexperienced programmer. Besides that, here are some advantages of using standardized authentication methods:
- The theory behind them is proven. Authentication algorithms are often developed by researchers in the academy. This means that the math behind them checks out.
- The implementation is easy. Most languages and web frameworks have open-source libraries with various stateless authentication methods.
- They’ve been test-proven in real-world applications. There isn’t much fragmentation in authentication libraries, since languages and web frameworks usually have only a few popular ones.
- They are well known by other programmers. As we discussed in the previous article, we want to build an API that is easy to use by fellow programmers. With a standard authentication method, people using your API will either have dealt with it before or easily find material online about it.
Let’s talk about some of these stateless authentication standards for HTTP APIs.
Basic Authentication
Basic Authentication is the simplest stateless authentication scheme that can be implemented in HTTP. The client authenticates users by including an Authorization
header with credentials on requests. The credentials are computed by joining the username and password with a colon (:) and encoding the result with base64. The result will be something like: Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
.
Due to statelessness, if the client wants to make successive authenticated requests, it needs to include the Authorization
header in each of them, i.e., the username and password will be re-sent on every request. This is a bit dangerous, because API clients like Single Page Applications or Mobile Apps need to store the username and password somehow, in case they want to keep the user logged in after closing the app. You don’t want to store plaintext passwords, even on the client-side, as this can be exploited by XSS or CSRF (if you use cookies for storage) attacks.5
Using Basic Auth with HTTPS is fine for simple or private APIs. However, other authentication methods are preferable for public APIs, like our bike rental API example. Other methods support useful functionalities such as credential rotation or using tokens instead of passwords after the first authentication request, which are useful to increase security in real-world production-level apps.
Token Authentication
Some web frameworks and libraries include a popular form of API authentication: tokens. Examples include Python’s Django REST Framework TokenAuthentication and Ruby’s devise_token_auth.
Usually, APIs that support Token Authentication have a “sign in” URL that accepts a POST request with username and password and responds with a token. A token is simply a string, usually randomly or cryptographically generated. Subsequent requests should use the token instead of the username and password, typically in the form of a header like Authorization: Token t$IKL&qNhTfx0pr$
. The token is mapped to the user in the server, generally in a database table.
The advantage of Token Authentication over Basic Authentication is that it decouples the authorization from the password, thereby supporting the following functionalities:
- No password: if the API client is somehow compromised, for example, a thief that stole the phone of a bike rental service user, the attacker won’t have access to the user password.
- Invalidation: again, if compromised, the token can be invalidated without changing the password. There are other purposes for invalidation, e.g., to revoke access after a password change.
- Multiple tokens per user: if necessary, a single user can have multiple tokens. A use for this is different tokens for different API clients: one for the mobile app, another for the desktop app, etc.
- Rotation: tokens can expire after some time (or even after each request) and be renewed, thereby reducing the attack window.
It’s important to note that not all Token Authentication implementations have all the functionalities just listed. Check the documentation of the framework or library you’re using. Also, please avoid rolling out your own Token Authentication. If you can’t find a good implementation, try using JSON Web Tokens, which are described next, since it’s a more prevalent standard.
JSON Web Token (JWT)
In simplified terms, a JWT is a standard, special type of token that holds a JSON payload with the data for authentication.
Here’s an example of an encoded JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.x8R6LPytDMrPuUBY71skyLBUrkme86DhioN3L7LY_-0
The encoded JWT looks like a random string, a opaque token. However, it actually holds data. Decoding the JWT above, we have the following JSON payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
“sub” means subject. It’s the identifier of the user related to that token, in this case, the ID of the user named “John Doe”. In JWT terms, “sub” is a standard “claim”, which is what the JWT developers call the access rights and other information stored in the JWT payload. “name” and “admin” are also claims, albeit not standard ones. JWT has other standard claims that can be used to implement useful Token Authentication functionalities, such as rotation.
You may ask: is this really safe? Can’t the client side change the payload and authenticate as another user or become an admin?. Yes it’s safe, and no, the client can’t change the payload. If they change the payload, the token can be recognized on the server side as false. The reason is that the JWT payload is cryptographically signed by the server, meaning it can’t be tampered with. This may sound a bit magical if you don’t know much about cryptography, but that’s the beauty of JWT. It can pass authentication information back and forth safely.6
Another great feature of JWT is that it supports authentication distributed in a multiple-server environment, like a microservices architecture. Since the payload can carry information about the user, a node does not needs to communicate with a central point to validate or retrieve information about the user on each request. From the token itself, servers can extract the user ID, profile info, permissions, etc. With this capability, even if the authentication service node goes down, other services can still operate normally. Relating this to bike rental, you can imagine that stations don’t need to make a request to authenticate users!
Note that the sequence of events in the JWT auth operation work just as a generic Token authentication: you have a POST “sign in” URL that receives a username and password and responds with a JWT, which is then used on each request to the server. Since JWT is a industry standard, it’s easy to find implementations for it in all popular programming languages and frameworks. It’s also pretty easy to generate the payload, because it’s JSON.
You can play with JWTs, generating them on your browser with the jwt.io debugger.
OAuth
If you have logged into some service using a social network profile like Google, Facebook, or Twitter, you were authenticated via OAuth. It makes sense to use OAuth when you’re a service provider like Facebook and you want to give other applications access to your users’ data. OAuth makes this possible by exchanging user credentials for an access token.
Suppose we implemented OAuth for our Bike rental service. This means that third-party applications can use the bike rental credentials to authenticate their users. Imagine a third-party Travel mobile app. In simplified terms, OAuth flow happens as follow:
- The user clicks on “Login via Bike rental” in the Travel app.
- The Travel app redirects the user to the Bike rental service URL for OAuth. This is a page served by the Bike rental service where the user will login with Bike rental credentials (if not logged already) and confirm the intention to authorize the Travel app.
- After confirming the authorization, the user is redirected back to the Travel app with an authorization code.
- The Travel app POSTs to the Bike rental service to exchange the authorization code for an access token. With this access token, the Travel app can get the authorized user’s data using the Bike rental API.
If you’re not a service provider that needs to share authorization with other third-party applications, OAuth against your own system is probably a overkill. It does have the advantage of decoupling the authorization server from the resources server, but this is probably too much for most APIs. Additionally, strictly speaking, OAuth is really an authorization standard, not an authentication standard.7 There are some pitfalls in using OAuth as an authorization standard, even though many applications do this. If you are going to use it, go with OAuth 2.0, an updated version of the OAuth with more features than version 1.0. For more info about OAuth2, check the community website.
A note About Same-Origin Policy and CORS
On browsers, there’s something called same-origin policy that restricts how a document or script loaded from one origin can interact with a resource from another origin. Origin here means the protocol, port, and host of a URL. If there were no same-origin policy, when you visited hack-me.com, it could make an AJAX request from your browser to your-bank.com and impersonate you using your cookies, since the request is coming from your browser! Same-origin also protects against dangerous DOM access, as you can check on Google’s Browser Security Handbook.
However, the stateless authentication methods we’ve discussed don’t use cookies. If no cookies are involved in the authentication, you can safely enable cross-origin requests in your API using CORS (Cross-Origin Resource Sharing). Public APIs are made to allow integration with external services, so enabling all origins access is part of their nature. Check for your web framework documentation on how to enable CORS. But remember: don’t enable CORS for all origins if you’re using any cookie-based authentication in your website. Also, if you have both a website version and a API-based version of a service, a good idea is to host them in different subdomains or domains. The website should keep same-origin policy active, while the API can have CORS enabled.
A note on HTTPS
HTTP (without S) is inherently insecure, since requests and responses data are transmitted in plaintext over the network. That’s why you must use HTTPS regardless of the authentication method you use in your API. HTTPS protects requests and responses in various ways, mainly against eavesdropping and tampering.
Now that you have an overview of the authentication methods available for RESTful APIs, you are able to make a good decision on which one works best for your application. Knowing how status codes and authentication work is already enough to start implementing great RESTful APIs, so congrats!
Conditional requests
This feature of HTTP is not used much in APIs, but can improve performance under some conditions. With some headers and status codes, clients and servers can control whether requests are fully processed, saving time and precious bandwidth. This is useful especially for caching and optimistic locking.
Caching
Respecting safety for GET leads to a gain in performance through caching. Caching means that not all requests need to be processed again by the server. If nothing changed in the resource, it’s OK to tell the client to use the most recent copy that it cached. Since what’s safe and what’s not is defined at the protocol level, caching is very generic and portable in HTTP. Because safe methods do not produce changes in the state of the server, we can freely (but carefully) add multiple layers of caching to our application.
There are many caching protocols. Let’s see some.
Caching with max-age
Here, we assume that a server response will continue to be up to date for a certain amount of time, such as an hour or a day. The client identifies that the last resource representation it received for a given request is still within the expiration time, so there’s no need to bother the server again and waste time waiting for it’s response. The way to do this is setting the Cache-Control header to a response. The most important parameter to know about is max-age=XXX, where XXX is the time in seconds the client can cache the resource.
It makes sense to use max-age only on resources that only change after a specific amount of time, or when it’s fine to have a out-of-date resource for some time. In our bike rental API, the list of stations won’t change too frequently. Therefore, it’s OK to set a small max-age cache for it, like a couple of hours. In the worst case, a user will have to wait some hours to see an update on stations, which may be fine depending on the requirements of the service. Also, to remediate this while keeping caching, clients can provide a “force refresh” functionality that ignores the cache and makes the request with the header “Cache-Control: no-cache”, which will tell all intermediate caches to ignore their copies and let the request hit the server.
Caching with ETag
Here the idea is to validate with the server whether the cached version of the resource is the same as the current server resource version. This can be made with a combination of two request/response headers: If-None-Match and ETag. The first time a request is made, the server will respond with a resource representation and an ETag header to identify the resource current version. The content of ETag is usually a MD5 hash of the representation, because hashing the representation is generally enough to identify the version of the resource. After that, the client will include this ETag value in the If-None-Match header of subsequent requests. Now the server can read the If-None-Match and check whether the hash of the current resource is the same. If it’s the same, the server responds with the 304 Not Modified status code, meaning the client can use the cached version of the resource. One might think that ETag caching isn’t useful because the request still needs to hit the server to validate the cache. However, you’re saving bandwidth when the server responds with 304 Not Modified, because there’s no body on those kind of responses.
Optimistic Locking
Imagine two people sharing a single account of the bike rental app. Imagine that both want to change the destination of a rent. They open the app in the rent page and select different new locations. Now imagine one of the users click on the “update” button. Before enough time has passed for this user to receive the response, the second user clicks “update” too. What happens? Well, if our application is not ready to deal with that kind of situation, both of them will think the destination will be set to whatever new location they selected, but only the later one will be right. The issue here is that the second user to hit “update” is making a change based in a version of the resource that is no longer valid.
One of the ways to solve this problem relies on taking advantage of the ETag header we’ve just talked about. We can use the HTTP header If-Match to pass the ETag value of the resource we are trying to update with a PUT request. If the ETag value has changed, the request will fail and the application can respond with a 412 Precondition Failed status code. In this situation, failing is better than misleading one of the users. This technique is called Optimistic Locking or Conditional Update.
There are more headers you can use for caching and optimistic locking, so to read more about HTTP conditional requests, check this MDN article.
Respecting HTTP Uniform Interface in status codes, method semantics of safety and idempotence, authentication, and conditional requests will help developers use your API. It will give them a lot better insights on what they might be doing wrong and how to fix their requests without the need to ask for support. Hopefully, this article helped you understanding about how to properly deal with this and it will spare you and your users a lot of time and headaches.
Next up, in the last article of this series, we will talk about various ways to structure our request and resource representations beyond vanilla JSON. We will discuss media types, and hopefully you are finally going to understand what the heck HATEOAS is.
- If a rent is created again with the same ID by another request, obviously another DELETE will change its state by cancelling it again. Idempotence is about repeating an operation and getting the same results, but only if the context hasn’t changed. Put in academic terms: idempotence is not closed under composition.↩
- GET /stations/ – Safe and Idempotent. Doesn’t change a station’s state.
GET /rents/ – Safe and Idempotent. Doesn’t change a rent’s state.
POST /rents/ – Not safe, but Idempotent. Changes the state on the first request when it creates a rent, so it’s not safe. But subsequent requests don’t change the state, because you can’t rent an already rented bike! Some POSTs can be idempotent and this is the case here.
PUT /rents/{id}/ – Not safe, but Idempotent. Changes the state on the first request when it changes the destination station, so it’s not safe. Subsequent requests don’t change the destination station again, therefore it’s idempotent.
DELETE /rents/{id}/ – Not safe, but Idempotent. Changes the state on the first request when it cancels the rent, so it’s not safe. Subsequent requests can’t cancel the rent that’s already cancelled, therefore it’s idempotent.↩ - Excerpt from Roy Fielding dissertation about REST.↩
- For more info about server-side sessions VS RESTfulness, check this StackOverflow question.↩
- For more info about CSRF and XSS on Stateless Authentication, check this StackOverflow question.↩
- It’s worth noting that JWT does not encrypt its payload. It does not hide or obscure the payload. The client can read the payload. It can’t change it, though.↩
- For more info about the difference, check this StackOverflow answer.↩