5 rules for better REST APIs
To avoid commonly made mistakes or bad design decisions and improve the overall design and effectiveness of your REST APIs, I’ve written down my personal “golden” rules when it comes to designing my REST APIs.
The REST paradigm is a very popular way of writing web APIs nowadays. But what’s so great about it? Here are some important reasons:
- Easy to understand.
- Makes use of the well supported and standardized HTTP.
- Simplicity due to a very limited amount of possible operations.
- Focuses on resources, not on actions.
1. The choice of verbs matters
REST supports a lot of HTTP’s verbs, and there’s a good amount of confusion about some of them among developers. Let’s shed some light into that topic!
The verbs GET
and DELETE
should be quite clear, as
they don’t need a payload and just do what you think they do. However people
often struggle with POST
, PUT
and
PATCH
.
All three verbs are used to manipulate data, either by creating new or changing existing resources.
POST
is used when it’s not guaranteed that the result of
the same input will be equal throughout multiple requests.
Take the following example:
Request 1: {"name": "John Doe"}
Response 1: {"name": "John Doe", "id": 1}
Request 2: {"name": "John Doe"}
Response 2: {"name": "John Doe", "id": 2}
Both requests contain the exact same payload (same input), however the responses carry different IDs (result).
PUT
is like POST
, however the result will
always be the same.
No matter how often you send {"name": "John Doe"}
, the result
will always be the same. This is called being idempotent; and yes,
POST
is not idempotent.
There can be exceptions, though. If the field is not part of the resource’s
defined state, then it’s alright to change them. Good examples are fields
like creation_time
and modification_time
, which are
auto-set by the API and have nothing to do with the resource itself. Instead,
they carry meta data.
PATCH
is used for updating parts of resources.
It’s especially useful if you don’t really care about the rest of the resource.
Due to those rules, we can connect the operations create, update and
partial update to the verbs POST
, PUT
and
PATCH
.
2. Design your API endpoints to be documents
Many developers design their APIs to reflect the internal database model (almost) 1:1, which is no good idea. Database models are usually designed to be normalized and atomic, which makes them flexible and efficient. Tools like database transactions ensure that the control flow including transaction safety is given and followed.
With REST, there’s no such thing like “transactions”, because every single request is executed in an isolated fashion (similar to auto commit when speaking about databases). Very often, however, you have to update nested (→ relational) data in one go; how does this work?
Let’s say you have the entities Order
and Item
.
Naturally you would create two API endpoints, namely /orders/ and
/items/.
Now a user creates an order plus 3 items. With the API design above, the following requests would have to be sent:
POST
to /orders/ for creating the order.- 3x POST to /items/ for creating items 1, 2 and 3.
That’s right: 4 HTTP requests. It’s not only the amount of requests that’s annoying, it’s also dangerous: What happens if the user’s internet connection dies right after the first 2 requests? That would leave a wrong order in the system.
The root of this problem isn’t REST, it’s this particular API design. Instead of just exposing the data model to the API, you should think in documents, i.e. embedded or nested data.
An order contains items, it’s a good old 1:n relation. So order items should be available under /orders/ as embedded data. Resource example:
{ "id": 1, "items": [ {"id": 2, "quantity": 3, "price": 2.5}, {"id": 3, "quantity": 1, "price": 5.0} ] }
Using that design, you can painlessly send full orders to the API server that either get saved as a whole, or not at all.
3. Provide extra data
One of the many complaints against REST is that it requires tons of round-trips to the server in order to retrieve the data you need. This can be solved.
For example let’s say we have an m:n relation, Post
and
Category
. An API design that requires lots of round-trips would
be like this:
-
GET
to /posts/:
// BAD BAD BAD! {"id": 1, "categories": [1, 2, 3]}, {"id": 2, "categories": [4, 5, 6]}
-
6x
GET
to /categories/ for fetching info about all related categories.
Of course this is ridiculous, so we better think of a better solution. Imagine this instead:
{ "id": 1, "categories": [1, 2, 3], "extra_categories": [ {"id": 1, "name": "Misc"}, {"id": 2, "name": "Food"}, {"id": 3, "name": "JavaScript"} ] }, {...}
extra_categories
contains read-only data that further explains
what categories
is all about (remember that you, of course,
still need the categories
field, because that’s what you use
when you want to modify data!).
The API can be designed to make all extra_
fields optional by
default and have the user request them when needed, thus reducing database
hits and bandwidth.
4. Avoid nested URLs where possible
Designing nested URLs is tempting, because it seems to solve the problem of working with nested resources. For example the URL of fetching a post’s categories could be /posts/1/categories/.
While this greatly reduces the number of round-trips to the API compared to the single request approach, it indeed makes the API more complex. Every endpoint adds to complexity, because that’s what the API user has to learn and understand.
Also, to be consistent (and you really want to be, because you want your interfaces to be predictable, right?), you would have to support constructs like these: /posts/1/author/posts/. At such things GraphQL shines, but not REST.
Instead, try to make your API design as flat as possible and trust your filtering options. That way your API will mostly behave equally everywhere and is therefore very easy to use and understand.
An exception to this rule is when you want to make something out of resources. For example /posts/2/word_list/ is a valid one, returning a list of words together with counts that are in the post (normally this is client code, but you get the idea).
5. Do not fake RPC through URLs
I know, it’s very tempting to do from time to time, but please refrain! RPC
(Remote Procedure Calls) are calls to remote functions that do something,
like create_order()
or get_order_as_pdf()
.
REST is limited to the well-known HTTP verbs, and that’s it. You shall not extend these! There’s always a way to represent actions as resources, you just have to rewire your brain sometimes. Examples:
"I have to ZIP up an order, how do I do it?"
- RPC style: /orders/2/as_zip
- REST style: /orders/2/?format=zip
"I need to execute some heavy export task, how to do?"
- RPC style:
POST
/orders/export/?ids=1,2,3,4 -
REST style:
POST
/order_export/ (where an "order export" is the export task itself, carrying status information and, when finished, result data or at least a link to it)
My point is: Even tasks or actions that are not directly connected to your database model can be expressed as resources. A call to an RPC function can be designed as a resource, too — if you wanted that!
Conclusion
Like anything else, REST has its pitfalls. Developers who design and implement web APIs should be aware of them, and also of ways that help avoiding them.
This article shows five very relevant rules that do away with prejustices and solve common problems people have with REST API design.
I really hope these rules help you build more robust APIs, and I’d love to hear your opinion in the comments, or just drop me a line at Twitter @stschindler
Have a good day!