The Art of Writing Amazing REST APIs
Originally posted September 2021 on the Split Blog
When writing APIs, REST (short for representational state transfer) is considered the standard. And yet, REST itself isn’t actually a standard. This makes designing intuitive REST APIs tricky to get right. It is a way of thinking or an art form more than a checklist. Having shaped API standards at two companies now, I can tell you that there are many components to creating a great API. There are some things that many companies get right and a lot that many get wrong. So how do I think about designing APIs? What makes a good API?
Consistency!
First and foremost, you’re creating an interface that someone else has to learn how to use. It is much easier to learn if you keep the same paradigms throughout that interface. Consistency extends from the big things, like how you divide your resources, down to the little ones, like how you name things. On top of that, it’s useful to also be consistent with other APIs your developers commonly use. If the developers are already familiar with one API, they will learn similar APIs more quickly. Fortunately for all of us writing APIs, this desire for consistency means that we can create a checklist or standard. Unfortunately, even with an extremely comprehensive standard, edge cases and weird situations always arise. For all of these edge cases, my response is to always go back to:
- What have we done before?
- If we’ve never done this or have been inconsistent, what do other APIs do? What is most common? Why do some companies depart from that (is it a bad design, or do they have a good reason)?
- What are likely/possible future use cases that may need to do something similar? What makes sense for them?
- What do I find to be the most intuitive and least confusing? What do a handful of other developers think is the most straightforward?
When you first put a standard in place for your API, it’s almost guaranteed that you already have at least a handful of public APIs. Despite this, you should still write the standard for your ideal case. If you could rewrite all of your APIs today, what would they be? After writing this standard for the ideal case, you will need a standard for the current API. This standard should prioritize consistency and can act as a bridge between your current APIs and your ideal state. The next step is to figure out how you want to either version or slowly shift your APIs to get your current version to that ideal version.
It’s All About Resources
Resources are one of the few things that REST does specify. REST APIs center around resources. The entire concept behind REST is that you’re exposing resources to the web. You provide actions on a set of resources. These resources should be completely stable within your API. By stable, I mean that every single endpoint that returns a particular resource should return identical representations of that resource. Different endpoints should not return different fields; the concept of that resource should always be the same. As a consumer of your API, I shouldn’t have to guess which fields I’m going to get back from an endpoint.
The only case where you might return something other than the complete resource is for relationships — for example, a resource that contains a reference to another resource (say, it has a parent object). In this case, it is acceptable to have a minimal representation of the referenced resource. This is especially desirable in a multi-service architecture, where the service for an API may not own all of the other resources referenced and should, therefore, only be responsible for returning its own data. This minimal representation should contain the information needed to pull the complete resource (typically that is two fields: type and id).
When designing these resources, keep in mind that your API resources DO NOT need to match the objects you have in your database. Think of it as your chance to redesign your data in a way that makes sense. Obviously, you also don’t want to put yourself in the position of having to do 20 complex database queries for each API call. However, where it makes sense, you can, and should, rename things and obfuscate confusing concepts. Just because you have three different database objects representing if a user was invited, confirmed, or archived doesn’t mean that API consumers need to know about it.
If it seems like the different actions on a particular resource are trying to do unrelated things, it may be that you’re conflating what should be multiple resources. While you don’t want everyone who calls your API to make ten calls every time they try to do anything, you also don’t want to conflate concepts. The more modular you can keep things, the more flexible your API will be for your users, and the easier it will be for them to understand.
IDs and Types Make the World Go Round
Use unique IDs on your APIs. Always. I have yet to find a case where a unique ID doesn’t work or make sense. I’ve seen the opposite too many times — where there is no ID and no good way to add one. All resources returned in a response should always include an ID. All GET endpoints should have an option to GET by ID. If you want to allow fetching a resource by name or something else, implement a query filter, but you shouldn’t skip the ID. In addition, this ID should be unique. It doesn’t have to be unique across all resources, but it should be unique across all objects of a given resource type (past, present, and future). If I delete one resource and try to fetch it later, I should never get a different resource. This ID doesn’t have to be a database ID — it could take several fields and combine or hash them together. In fact, it doesn’t matter what the ID is as long as it uniquely identifies an object.
Similarly, you should always return a type field with your resource. This may seem redundant — you called GET /flowers, so it seems obvious that the response would have “type”: “flower”. However, there are good reasons for returning the type. For any endpoint or field that could return more than one type, the type field identifies the type of each of those resources. Additionally, it enables the user to fetch the complete object (since you know what endpoint to use). As an example, you may have an approvers field on a change. These approvers could be either users or groups. The type allows you to distinguish between them. Including type also gives you future flexibility. Just because you don’t allow group approvals right now doesn’t mean that you won’t add them in the future. Having the type present allows you to change that without it being a breaking change. If it wasn’t there, anyone writing code against your API would have assumed a type previously. If this type can no longer be inferred, any code with that assumption will break. It is the case that type is only helpful in some scenarios, and it can therefore be tempting to only use it for the cases where it’s needed. The counter-argument goes back to consistency and stable resources — your resources should always have the same fields. This also extends to type. It’s easier to always include type than to not have it when it could have made all the difference.
Resource Names are Important
As I said before, resources are the soul of your APIs. However, if no one knows what they represent, they can be almost useless. Names should accurately and clearly articulate what a resource represents. Additionally, they should ALWAYS be plural nouns. Resources are objects, so they should always be nouns because objects are nouns, not verbs.
But why plural? The way I think about it is that a resource endpoint doesn’t represent a single instance of the resource. Instead, it references the pool of all of that resource type on your server. For example, the endpoint /plants represents all of the plants on the server. GET /plants should return a list of those plants. POST /plants adds an item to that pool of plants — I’m adding something to plants. Even in the case of GET /plants/{plantId}, which is expected to only ever return one item (because of the unique ID, remember?), it’s narrowing down that list of all plants to a specific one. It’s like saying that of all of those plants, filter it to the one with this ID. Therefore, resources should always be plural nouns. Likewise, I could do /plants?flower_color=blue, which should return a list of all plants with blue flowers.
Follow Related Standards
While REST doesn’t have a standard, it does use some other protocols that do have standards. For example, most people write their REST APIs using JSON and HTTP. Neither of these is required, but both, especially HTTP, are nearly universal. If you use JSON and HTTP, you should adhere to their standards. The JSON standard is straightforward. The HTTP standard, meanwhile, is unsurprisingly quite lengthy. There’s a lot in there. There are a few things, specifically, that I want to make sure to point out. Those are the correct use of status codes and the correct use of HTTP Methods. These are both complicated enough that I’ve written separate blogs for both status codes and HTTP Methods. Getting these right is essential for any good API.
Completeness
People often consider it to be the next level of REST or truly RESTful if you’ve implemented HATEAOS (Hypermedia as the Engine of Application State) links. That said, HATEAOS links, while cool in concept, are not useful for how most people consume modern APIs. Therefore, in almost all cases, I would recommend against implementing them. That said, I do still like the idea behind HATEAOS. To summarize here, the main idea of HATEAOS is to create a way to traverse the API without any prior knowledge of that API except for a starting point. Each response contains links to related resources and so forth. I don’t think HATEAOS links are useful because almost any reasonable person building against your API will be looking at your documentation. A lot. They are also writing stable code that relies on specific, known endpoints, not randomly traversing things.
However, while you might not build HATEAOS links, you should be sure that a user can traverse from resource to resource. They should be able to complete workflows through the API alone. This means that if I reference a parent folder in my file resource, I should also have a GET endpoint to fetch that parent folder. This reiterates the importance of IDs. If I list groups associated with a user, I shouldn’t list just the group names; I should include the ID so that an API user can gain more information about or manipulate those groups. They should be able to walk through the data from resource to resource.
Most APIs contain some holes and lack some parity with their UI offerings, but it’s ideal to limit gaps where possible. Adding APIs can be time-consuming, so when making tradeoffs, I would consider making all APIs available for a given resource rather than adding a few APIs across a bunch of resources.
Part of the beauty of APIs is that they open up functionality possibilities never even dreamed of by the original company. They allow developers to extend functionality beyond the use cases provided in the UI and create entirely new flows. With that in mind, don’t just fill in the APIs for the use cases you know about, but try to fill in as many APIs as possible, starting from the core objects.
Endpoint
With everyone talking about REST, but no actual standard, it can be challenging to write good, usable, and extensible APIs. It can take time and practice for design to come easily, but there are specific things that are important to keep in mind. Your APIs can be improved by taking the time to carefully think through resources — always using plural nouns, making sure to include type and id, and following related standards. You can further enable the developers to use your API by providing APIs for as much of our product as possible. Finally, above all else, consistency across your API and with other APIs will make your APIs easy to use.
To Learn More About Building Great APIs
- Read more about status codes and HTTP Methods in the separate blogs that I’ve written about each
- This book is a great foundation on REST APIs