Here I want to look briefly at what a REST API is and offer some advice on how to structure one, how it should behave and what should be considered when building it. I know this isn’t emacs vs vi, but it can be quite contentious. So, as Barbossa from Pirates of the Caribbean said, this “...is more what you’d call ‘guidelines’ than actual rules.”
Resources & Identifiers
In their book, Rest in Practice - Hypermedia and Systems Architecture (ISBN: 978-0596805821), Jim Webber, Savas Parastatidis and Ian Robinson describe resources as “...anything we expose on the Web, from a document or video clip to a business process or device.”
They go on to describe identifiers as a way to identify a resource on a network and make it referenceable, often by way of a Uniform Resource Identifier (URI). The identifier allows the resource to be manipulated by way of Hypertext Transfer Protocol (HTTP).
Behaviour
Throughout the book, Webber & Co. use the example of a fictitious online coffee ordering system called Restbucks, where the resource is an Order. The Restbucks service is a good example of a well defined REST API and demonstrates clearly how to identify and manipulate the Order resource:
Verb | URI | Use |
POST |
/order
|
Create a new order, and upon success, receive the new order’s URI. |
GET |
/order/{orderId}
|
Request the current state of the order specified by the URI. |
PUT |
/order/{orderId}
|
Update an order at the given URI with new information providing the full representation. |
DELETE |
/order/{orderId}
|
Remove the order identified by the given URI. |
The verbs dictate either how the resource will be manipulated (POST, PUT, etc.) or that it will be retrieved (GET). The response is also part of the behaviour and has two parts, the body and the status code. The status code indicates what has happened and whether it was successful.
REST APIs
Let’s take a look at what we should consider when creating a REST API.
Consistency
Probably the most important property of a REST API is consistency. Identifiers, resources and behaviour should be consistent throughout the API, unless there is a good, well documented, reason for them not to be.
For example if a resource supports an optional year query parameter and, if omitted, it is defaulted to the current year, then all the identifiers for that resource which support an optional year parameter should default to the current year when it is omitted. If the API supports multiple resources with optional year query parameters, they should all behave the same way unless there is a good, clearly documented, reason not to.
Documentation
All resources and identifiers within an API should be documented, using something like Swagger (https://swagger.io/), and published. Especially if the API is exposed for public consumption or for consumption by another team.
It is important to include the request and response messages, possible response codes, all query parameters and required fields.
Resource Naming Conventions
Resources and identifiers must be consistently named across the API. If the API supports multiple resources, the pluralisation, or not, of the resource name should be the same for all resources across the API.
Bear in mind that GETs can usually return more than one resource, whereas POST, PUT and DELETE usually operate on a single resource at a time, but can operate on multiple resources. This means that a decision must be made whether to name the resource in the identifier as singular (order) or plural (orders). It doesn’t matter which, but must be consistent (my preference is for singular).
Versioning
Significant and breaking changes to an API should result in a new version of the API, with the previous versions remaining in place, but deprecated, for existing consumers who won’t or can’t accommodate the change immediately. Once the client has made the change, the deprecated API versions should be removed.
There are at least two ways to version an API:
- In the header
- As part of the URL
While making the version part of the URL might be considered ugly, it has a number of advantages over putting the version in the header:
- The service has separate endpoints for each version, rather than one endpoint which must check the header to know what to do. This makes the service easier to maintain.
- It’s clearer to see which version of an endpoint has been called when debugging, as the header doesn’t also need to be checked.
- The URL is more likely to be logged than the header, so an explicit header log, which may contain information you don’t want in the logs, is not necessary.
- The URL is more likely to be cached on its own, than with the headers, so a change in the header is unlikely to be sufficient for the cache to be reset, and you may continue to get the old version.
Verbs
Operations performed or resources should conform to the five main verbs:
- GET - Retrieves a resource(s)
- POST - Creates a resource(s)
- PUT - Updates a resource(s) and, in certain circumstances, creates a resource(s)
- PATCH - Updates part of a resource(s)
- DELETE - Deletes a resource(s)
PUT should only be used for updating a resource(s), except where the resources(s) might already exist. Then it can be used to update or create a resource(s). A good way to think about PUT is “make the entity state like this”
POST can be used instead of GET when a large number of values need to be passed, for example when creating a report, and must go in the body. This is especially important when the maximum URL length may be exceeded, or you want to avoid putting Personal Identifiable Information (PII), such as email addresses, in the query.
Remember that, although some implementations support it, a DELETE should not have a body. Any specification of what should be deleted must be passed via query parameters.
Responses
A response body, in most cases, should return the resource which was requested, created or updated, and/or the URI at which the resource can be found. Clearly the resource needs to be returned in the body of a GET, but it’s optional for the other verbs. Whether to return the URI as well or instead comes down to personal choice, but be consistent.
The status code 204 No Content can be returned if a resource(s) is deleted.
A response may also include paging information or other information generated as part of the operation, such as counts.
Custom Error Codes
A service may have more specific information about why a call has failed. To help the client identify this, the response should include both an integer and an accompanying string describing the failure. Both the integer and string should be documented in Swagger.
A success field in the response body, indicating whether the operation succeeded, should not be returned. This is what the status code is for and where necessary a 207 multistatus (see below) can be used.
Headers & Identification
A client should identify itself by including an X-APP-ID in the header. The service should make this mandatory for all endpoints and fail the call with a 403 Forbidden if it is not supplied. It is often useful, especially when upgrading, moving or migrating a service, to know who is calling it.
HTTP Response Codes
There are lots of HTTP response codes:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
While they all have their uses; only a handful of them fit into most scenarios. For example, most endpoints in an API should be covered by:
- 200 - OK
- 201 - Created
- 204 - No Content
- 207 - Multistatus
- 400 - Bad Request
- 404 - Not Found
- 409 - Conflict
- 500 - Internal Server Error
I’ve included some further status codes below which should also be considered. The descriptions from the Mozilla website makes them self-explanatory, but 207 multistatus, 400 bad request and 500 internal server error require some further explanation.
400 Bad Request, should be used when the message passed to an endpoint is invalid, either in structure or in the data which is passed. For example, when adding a coffee to an existing order, if the type of coffee (a required field) is not supplied, then a 4oo should be returned. However, if the order does not exist a 404 should be returned.
500 Internal Server Error should only be returned if something has failed, for example an exception is thrown due to the database or a third party service being unavailable.
207 Multistatus should be used if part of the request has succeeded and part has failed. For example if a number of orders are updated in one call and some of them update successfully, but some of them fail due to a validation error, an overall status 207 should be returned with details in the body of the individual successes (200) and failures (400), including the appropriate status code for each. There is further explanation here: https://www.rfc-editor.org/rfc/rfc4918#section-13
Success responses
200 – OK
The request succeeded. The result meaning of "success" depends on the HTTP method:
- GET: The resource has been fetched and transmitted in the message body.
- PUT or POST: The resource describing the result of the action is transmitted in the message body.
201 – Created
The request succeeded, and a new resource was created as a result. This is typically the response sent after POST requests, or some PUT requests.
202 Accepted
The request has been received but not yet acted upon. It is noncommittal, since there is no way in HTTP to later send an asynchronous response indicating the outcome of the request. It is intended for cases where another process or server handles the request, or for batch processing.
204 No Content
There is no content to send for this request, but the headers may be useful. The user agent may update its cached headers for this resource with the new ones.
207 Multi-Status
Conveys information about multiple resources, for situations where multiple status codes might be appropriate.
Client Error Responses
400 Bad Request
The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
401 Unauthorized
Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
403 Forbidden
The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.
404 Not Found
The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
405 Method Not Allowed
The request method is known by the server but is not supported by the target resource. For example, an API may not allow calling DELETE to remove a resource.
409 Conflict
This response is sent when a request conflicts with the current state of the server.
Server error responses
500 Internal Server Error
The server has encountered a situation it does not know how to handle.
504 Gateway Timeout
This error response is given when the server is acting as a gateway and cannot get a response in time.
503 Service Unavailable
The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This response should be used for temporary conditions and the Retry-After HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached.
Great post Paul!
ReplyDeleteI agree with most of what you are recommending here... personally I prefer to pluralize my resources. Mainly because I like to think of it like a file & folder structure where you would have a "Documents" directory containing a file called "file1" which is accessed via "Documents/file1. "Document/file1" just doesn't sit right with me, particularly if "/Document" returned and array of files! However as you point out Consistency is key. There's nothing worse than reading through the documentation of an API (assuming it exists!) and seeing a mismatch of conventions!