BattlefyBlogHistoryOpen menu
Close menuHistory

Hot take: REST is a waste of time, just do RPC

Ronald ChenMay 23rd 2022

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.

REST APIs fail to deliver

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.

No standard way to implement pagination, let alone filtering/sorting on top of that

  • Paginate by url? /wishlist/1234/page/3
  • Paginate by query parameter? /wishlist/1234?page=3
  • How does the client know the total number of pages? Data in body? Data in header? Data on a different url?
  • How does the client know previous/next page url? Are the urls returned by the API or is the client expected to know how to construct urls? Are the urls in the body or header?
  • The unresolved issues with previous/next page url are the same unresolved issues with all links with HATEOS

What is and isn't a sub-resource?

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?

REST is actively harmful

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.

RPC is the simplest thing that works

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.

  • get page 3 of wishlist 1234
  • update description for user 1234

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:

POST /command/:functionName
Authorization: ...
Content-Type: application/json
...JSON object of function arguments

The backend routing is simple, given a service:

app.post('/command/:functionName', async (res, req, next) => {
  try {
    const {params: {functionName}, body} = req

    const returnValue = await service[functionName](body)

    return res.send(returnValue)
  } catch (error) {
    return next(error) //error handling middleware internally logs the error with trace ID and returns status 500
  }
})

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.

2022

Powered by
BATTLEFY