Design your API with objects, not actions
Designing an API is hard. You think of endpoints as you imagine them; implement them and sometimes sooner or later, you realise things aren’t built the way they should have been.
While this is inevitable and happens with every bit of software, it’s harder with an API because you cannot change it so easily as it will break any client using it.
For a public API, rolling breaking changes can take several years.
Even for private ones, when several internal clients are using an endpoint, migrating to something better can take quite a long time.
Even when building with those constraints, there might be times where something seems to be following them, but could be done better.
Thinking with objects
Let’s take the standard blog tutorial. You have created your new API for this awesome blog of yours, and you can create blog posts.
Due to REST, this is fairly straight forward and you end up with a few endpoints.
|GET||/posts||Get the list of all posts.|
|GET||/posts/:id||Get a post by id.|
|POST||/posts||Create a new post.|
|PATH||/posts/:id||Update a post by id.|
|DELETE||/posts/:id||Delete a post by id.|
This works, it’s clean and neat. You’re proud of yourself and can be.
Now, we want to add a new feature: subscribing to a blog post.
When someone subscribes to that blog post, updates will be sent to them.
Take a few seconds to think of how you would implement that subscription.
You can take that time to look at this awesome GIF.
Chances are you thought of the following endpoints:
|POST||/posts/:id/subscribe||Subscribe to a post by id.|
|DELETE||/posts/:id/unsubscribe||Unsubscribe from a post by id.|
This looks nice at first, though there are a few problems.
- Nested URIs are usually fairly complicated to maintain. What if tomorrow you want to be able to subscribe to several posts at the same time?
- There is an inconsistency in the naming. You first have
- This code is built with actions, not objects.
To improve this, let’s first try to understand what it does.
Internally, you probably have two database tables.
posts, which contains specific posts data.
subscriptions, which links a subscription to an email address.
When you subscribe or unsubscribe from a post, you’re going to create or delete a subscriptions object.
Creating a subscription probably just executes the following SQL query:
INSERT INTO subscriptions (post_id, email) VALUES (42, 'firstname.lastname@example.org');
We’re just creating a new object. So why think with “subscribe” or “unsubscribe”?
Those terms would just be for end-users, so they see what action clicking on a button will do.
But an API is not for end-users. It’s for machines.
If we were to think in term of objects here, we would create the following endpoints:
|POST||/subscriptions||Subscribe to a post.|
|DELETE||/subscriptions/:id||Unsubscribe from a post.|
This is much better in many ways.
- We fully respect REST, as we’re creating objects and calling them by their ID (unsubscribing by email should be fine too).
- Consistancy is maintained. All endpoints have the same namespace.
- This code thinks in term of objects, not actions.
When you come back later to these endpoints, you can much more easily extend them to better apply with your potential future needs.
You can easily add
GET endpoints to fetch either a specific subscription (am I subscribed to this post?), or all of them (who is subscribed to which post?).
You can easily update this to only allow authenticated users to create subscriptions.
Things are much clearer, as you’re representing everything the same way.
As mentioned at the beginning of this article, designing an API is very hard, and there is no choice but to doing with trial and error.
You will probably end up breaking things with time.
But if you can avoid that with simple tricks like this one, you should mostly be fine.