I've done my fair share of level 2 RESTful APIs and even some level 3 HATEOS. Over time, I've come to the opinion that REST APIs fail to deliver on their promise. Furthermore, in most cases a REST API is actively harmful and it degrades to busy work.
So we should just be using GraphQL right? Heck no. GraphQL is a cure that is worse than the disease.
My hot take is, we should simply use remote procedure calls (RPC) and it is the simplest thing that works.
The reason why RESTful APIs are so attractive is because of standards. We can be far more productive if we could focus what API we are building instead of how we are building it. In theory, if we built a RESTful API perfectly we could interoperate with other tools. In practise, this does not work.
We all like to think we have a shared common understanding of what it means to be a RESTful API, but there is no such thing. The problem is the definition of RESTful APIs is incomplete and we all fill in the blanks in different ways.
/wishlist/1234/page/3
/wishlist/1234?page=3
If the API offers a resource /user/1234
as a JSON object with a description
field, is the API required to offer PUT /user/1234/description
to update the description
? Or is the description
only updated about by updating the whole user with PUT /user/1234
? Or is the description
updatable with a partial JSON object using PATCH /user/1234
?
Some have the idea that we ought to build a RESTful API for our single page application, as that would become our public API in the future. Not only does that day never come, I am reminded of the quote "To do two things at once is to do neither". This idea is especially insidious as one of the promises RESTful API tries to make it the ability for API consumers to discover what the API has to offer. This is irrelevant when the API consumer is a coworker who has access to the source code! Moreover, even if this became a public API we saw in the previous section, one would need detailed documentation anyway! What is the point of this discoverability if people need to RTFM.
If we are not deluding ourselves into think we are building a possible future public API, then we can focus on building the simplest private API. The private API doesn't need to be cohesive, it just needs to get the job done in the most straight forward way. We can decide as a team how we are going to do pagination and not worry how to appease the RESTful gods.
There is no need to decide what is or isn't a resource/subresource or even what should our URL structure should be like when doing RPC.
RPC is easy to implement over HTTP. Just use POST
for everything. I have angered the RESTful gods. But hear me out.
Let's think about what we are really doing when building a single page application. It is an UI which sends a series of commands to the backend.
If we ignore HTTP for a bit, we can think of these commands as functions.
function getWishlist({wishlistID: ID, page: number}): Promise<WishlistPage>
function updateUserDescription({userID: ID, description: string}): Promise<void>
These functions might as well be the actual functions as implemented in the backend. If that's the case, what is the simplest way for the frontend to call these functions? Ideally, these function types would be the same in the frontend and in the backend. Developers wouldn't need to do some RESTful translations to figure out which backend function gets called in the end when a particular frontend action is performed.
As for the actual RPC over HTTP transport, it simply maps to a single endpoint:
The backend routing is simple, given a service:
The transport layer would need to implement ACL to ensure the authenticated user in the frontend is authorized to call the function in the backend.
Don't fret over status codes. Only 2 status codes are required for RPC over HTTP, 200
and 500
. 200
means the RPC was successful and 500
means there is an unknown failure as part of the transport. For business logic failures, the error should be in the body with status 200
. This simplifies API consumers as there is no RESTful translation required on non-200 response. RESTful API would require clients to handle status 400
and extract the bad field into the UI. All that goes away and now the definition of the possible error resides in the function return type.
What about thrown errors? What if the backend function throws an exception? In my opinion, an exception should result in a failed RPC call with HTTP status 500
with no additional information asides from a trace ID. It is dangerous to expose backend exceptions to the frontend, as this could potentially leak personally identifiable information or secrets. Design functions to return error objects for expected violation of business rules (e.g. promotion has expired). Unexpected errors can be carefully refactored as error objects in function returns.
RPC allows us to design in terms of function parameter and return types, as opposed to encoding what we really want to do into REST.
And there we have it. Transparent RPC over HTTP. Less REST bike shedding. More productive work.
Do you disagree? Do you want to debate the merits of REST vs RPC? You're in luck, Battlefy is hiring.