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:

  1. GET to /posts/:
    // BAD BAD BAD!
    {"id": 1, "categories": [1, 2, 3]},
    {"id": 2, "categories": [4, 5, 6]}
  2. 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!


This article was published on Jan. 19th, 2017 by stschindler and is tagged with