Guys, REST APIs are not Databases
One of the common mistakes developers make regarding REST APIs is to treat them as databases. They’re not! Visit any web framework documentation and at some point you’ll bump into a magic way of mapping database CRUD operations of ORM models with REST API endpoints. Each letter of CRUD becoming HTTP verbs:
C = POST
R = GET
U = PUT or PATCH
D = DELETE
I am the first to admit that these operations naturally match each other, which justifies combining them. However, the problem begins when developers start associating REST APIs with database concepts and miss the whole point of REST. The purpose of databases is to store data, and APIs are all about how components interact with each other. Another problem is that if you build an API around database concepts there’s a high chance that over time it will become ambiguous and hard to maintain because that’s what naturally happens to database schemas, they get more and more generic and ambiguous over time because they need to serve different contexts and by consequence get stretched by them.
REST means REpresentational State Transfer. You can take it literally, it means when you do REST you transfer the state of something over some protocol (HTTP is the common choice). The “something” that gets its state transfered is also known as a “resource”. And the “state” is like a snapshot of the resource. Can the resource be directly mapped to an ORM model? Sure it can! But this “resource” might be many things, not only an ORM model. For example, it’s very common for some resources to be a read-only compilation of data.
An interesting mind game to dissociate REST APIs from database concepts is to imagine how you would create endpoints for a Time resource. Certainly Time wouldn’t need to exist in the database. You could use GET to read the current time, you could also use POST to create future dates by adding days to other dates. And so it goes, databaselessly.
So how should REST APIs be built then?
Build APIs around user contexts
Imagine an API that is consumed in very different ways by each user. For example, a shipping company that has an API with a Shipment resource in it. Do you think Shipment will be seen the same way by a Buyer, a Seller or the Carrier? Absolutely not! These users have views of the Shipment resource and need to do different operations on the API. The Buyer wants to pay and track the shipment. The Seller on the other hand is only worried about inventory and shipping fast. The carrier cares about the dimensions and weight of the package and its destination, so it goes. If you build this API as an one-in-all solution for these three users you’ll end up with an ambiguous and over-stretched API that will be difficult to evolve, hard to maintain and will expose more data than it should. In this case you should build three specialised APIs, one for each user context. Because of the hight coupling on data level these three APIs should live in the same project, most modern web frameworks support subdomain or prefix routing, implementing it shouldn’t be a problem. It might sound like too much work but over time it will pay off. An alternative to contexts would be implementing permissions or some sort of access control on the API, I’m a bit wary of this approach as it adds unnecessary complexity in my opinion.
When contexts are not possible
If an API has so many users that it makes it impossible to have contexts the right thing to do is to accept this reality and embrace it, but do your best to help the API users. For example, you could use technologies like GraphQL to empower users of what they ask and get from the API, this little control can make all the difference.
But my API is too small, stop overcomplicating things!
Ok ok, there’s a direct relationship between size and scope. The smaller an API is the less scope it has, it’s a no-brainer. Thus if an API is too small it will be ambiguous and generic by design and won’t need contexts, you can keep it simple and do your CRUD operations mapped to REST endpoints. But keep a close eye on how it evolves, if you find yourself filling up the API code with conditionals and other types of patching consider the approaches above.