Best practices for RESTful API design

7/12/2018 - Joep Meindertma

The internet started off as a place for linked documents. However, where humans are perfectly capable of understanding HTML documents through a browser, this does not apply for machines that need a specific piece of information. That’s why every decent web service has an API, and probably uses it for their website, app, integrations and external services. Unfortunately, too many APIs are unnecessarily hard to use and unintuitive. In this article, I'll give some practical advice on designing a RESTful, hypermedia API that follows web conventions.

Use URLs as IDs

Every thing in your API, every individual resource, should have its own URL. The URL should serve as both an identifier as well as a locator: it is the identity of the thing and it provides a way to fetch information about that thing. This makes URLs really useful.

Firstly, URLs make your responses far easier to navigate. Imagine a JSON object representing a social media post that references some author with an identity of 18EA91FB19. From this information, you wouldn't know where to find that author. You need to read the API docs, discover the endpoint for authors and compose your request. If the ID was a URL, you would instantly know where to send that request to. This is not just great for humans, but also for machines, since they can't read your API docs - but they can navigate URLs.

Secondly, URLs are always unique. URLs are not just unique identifiers in a single system, but also unique across different systems. The domain name takes care of that. This means that you can use your data across multiple systems without worrying about name collisions in identifiers. This is one of the properties that makes linked data awesome.

Make sure that your URLs are stable. Cool URIs don’t change. If they really have to change, make sure the old URLs redirect to the new ones. Nobody likes broken links.

Your API endpoint is your website

You don't need a subdomain for your API, like api.example.com or a sub-path, like example.com/api. Your endpoint should be the root of your webpage: example.com.

This is useful, because as discussed above the URL should be both the identifier as the locator of a single resource. Whether someone is looking for a HTML version or for example a JSON representation of a resource, he should be able to use the same URL. This makes your API easier to use, because someone who navigates your website can know at any time how to access the same resource in some other format.

But if the URL does not change across format, how do you request the right one? This is where HTTP content negotiation comes in handy. A client can send preferences about what kind of content it wants to receive in the Accept HTTP header. The default header for web browsers is text/HTML, but for most APIs, a machine readable setting such as application/json is more suitable.

But what about API versioning? We want our URLs not to change, so we should not use different URLs for different API versions. The solution, again, is to use a HTTP header. Use an api-version header or a specific Mime type in your requests.

Use sensible hierarchy in URL paths

Having a URL hierarchy that makes sense is not just important for your website, but also for your API - especially if your API structure resembles your website structure. Try to come up with a sensible URL strategy, discuss it with your colleages and do all of this early in the development process.

A few things to consdier:

  • Move from large to small, from generic to specific.
  • The user should be able to remove the last part of the URL and arrive at a parent resource.
  • Let the hierarchy reflect the UX of navigating the website.
  • Try to keep URLs as short as possible.
  • Human readable URLs are easier to understand and share, and they are great for SEO.
  • Cool URIs don't change. Leave out anything that might change, such as author, file name extensions, status or subject.

Use query parameters correctly

The URI spec tells us to use query parameters only for non-hierarchical data.

Don't use query parameters to identify a resource; use a path.

  • Bad: example.com/posts?id=123
  • Good: example.com/posts/123

Use query parameters for optional things like limiting, sorting, filtering and other modifiers:

  • Good: example.com/posts?limit=30
  • Good: example.com/posts/123?show_hidden=true

Use HTTP methods

Instead of having a bunch of endpoints for various types of actions, use a single URL for every single resource in your application. Distinguish between actions using HTTP methods.

  • Bad: GET example.com/showPost/123
  • Bad: GET example.com/removePost/123
  • Good: GET example.com/posts/123
  • Good: DELETE example.com/posts/123

There is a big difference between requests that aim to read content, create content or edit content. Make sure to use the GET, POST, PUT and PATCH HTTP methods correctly. The GET and PUT operations are idempotent, which means that a request can be repeated multiple times without side effects. This distinction is important, because it tells the client whether it can try again if an error occurs. It also helps with caching, since only GET request should be cacheable.

If you want to offer a form to delete or edit a resource, that form will be a different resource from the original item, so it will need a seperate URL. A nice convention is to nest that form resource below the original item. This way, the user just adds /edit to the a URL if he wants to edit that resource.

  • Good: GET example.com/posts/123/remove
  • Good: GET example.com/posts/123/edit

Use HTTP status codes

Pretty much all types of error messages can be categorized in the existing HTTP status codes. These are not just useful to humans, but especially to machines. Status codes can be parsed far more quickly than a body text. Another advantage is that they are standardized, so the client library is likely to know what the status code represents. You don’t have to support every single one, but at the very least make sure that you use the five categories:

  • 1xx: informational - just letting you know
  • 2xx: successful - everything’s OK
  • 3xx: redirection - your content is somewhere else
  • 4xx: client error - you’re doing something wrong
  • 5xx: server error - we’re doing something wrong

Add context to your JSON

Assuming you use JSON as a serialization format, you can use @context. The @context object is a nifty little idea to make your API more self-descriptive. It describes what the various keys in your JSON actually represent. It provides links to where the definition can be found.

Make sure all your IDs are actually links, and your context is included. Now all your JSON has become JSON-LD, which is linked data. That means that your JSON data is now convertible to other RDF formats (Turtle, N3, N-triples, etc.), which means it becomes far more reusable.

Keep in mind that the links that you use should preferably resolve to some document that explains what your concept represents. A good starting point to find relevant concepts is schema.org.

Offer various serialization options

Be as flexible as possible in your serialization options. Someone who uses your API might be good at dealing with JSON, but may not know how to work with XML. Serializing an in-memory object to various serialization options is often not that complicated. If you use Ruby on Rails, check out our rdf-serializers library which supports JSON-LD, RDF/XML, N3, N-triples and Turtle. Use the aforementioned HTTP accept header to handle content negotiation.

Standardize index pages and pagination

You’re probably going to need index pages with pagination. How to deal with that? Pagination is not a trivial problem, but luckily for you, you’re not the first to encounter it. Don’t try to reinvent the wheel; use something that already exists, such as W3C activity stream collections or Hydra collections.

Don’t require an API key

Your default API (the HTML one) doesn’t need one, so your JSON API shouldn’t need one as well. Use rate limiting to make sure your servers don’t fry. You can still use API keys or authentication to give access to special parts of your API, of course.

Use a doc. subdomain for API docs

Here's a clever little idea: make your API documentation available at doc.example.com. If a user wants to know how your api works for a certain page, he just adds doc. in front of his current URL. Show the user a page that tells something useful about how to use the API at that route.

Use your own API

Finally, and perhaps most importantly: eat your own dog food. Make your API a first-class citizen by using it as the only way to access information from that system. API-driven development forces you to make your API actually work. It helps you document your API properly, since your colleagues need to use it as well. Besides, you'll make your application more modular and gradually realize a microservice architecture, which has its own set of benefits.

Let me know if I'm missing something, or if you want help with API design!