Experimenting with RPC for UI/Backend Communication

REST seems to be the stand-by choice to organize the wiring in a typical single-page application. But is that actually sensible or just a by-product of industry inertia?

REST is deservedly well-known as the preferred way to architect external-facing consumable APIs for third-party integrations over HTTP. It is also great for structuring microservice contracts. But for closely integrated frontend-backend interaction in a normal web-app the benefits are less clear — or at least there is some room to experiment.

So, in March 2015 I decided to dig up some findings on that.

I was starting a new side project at the time, a simple client-side app that needed to make a handful of calls to trusted server code. I got cheeky and chose to connect UI and backend code using only WebSockets — not because it was practical, but because it let me focus explicitly on an alternative to the REST paradigm. Of course, this is nothing new — frameworks like Meteor already use WebSockets as their primary network layer. But in this case I specifically wanted to play with RPC as a concept.

Typically, in a “rich web-app” the client-side code is loaded in the browser via CDN or same server as the backend. Possibly pre-rendered markup, possibly not. And then that client-side code uses XHR to make calls to the backend to authenticate, load dynamic data, submit user actions and form data.

Because UI and backend are so tightly coupled and versioned together for “full-slice” development, I could never justify to myself all the worry about verb usage and URL resource naming. In fact, reusing the same URL resource for multiple views has proven to be an anti-pattern of sorts, because it has been cumbersome to track which view used which fields of that URL resource, hence introducing refactor inertia.

This is why RPC appealed to me, as a behaviour-oriented rather than resource-oriented paradigm.

The RPC over WebSockets experiment proved to be quite fruitful.

I ended up creating a quick library called Remote Control for Node.js: at its core it simply handles WebSocket connection setup, simple request multiplexing and bare-bones JavaScript data structure encoding/decoding on the wire. But there is more.

The important feature of RPC is that it allows to “forget” about the network layer and treat client and server as close-knit. Client code versus backend code are easier to think of as just different places in the call-stack then. To enable that, I added very simple interface introspection to Remote Control, allowing server-side JavaScript methods to be exposed as equivalent client-side callable functions.

Opinion rant detour: folks often fixate on “frontend” versus “backend” as defining layers of software architecture. I try to look at it as “presentation” versus “model” first, and then as “trusted” versus “tamperable”. Often there is a bit of presentation-specific code on the server — not because it has to be trusted against tampering, but because of stack shortcomings. And similarly, not all model code has to be tamper-proof: a simple mobile game does not need server-side validation for its state. All of this needs subtler adjectives to classify. In larger codebases, these nuances can even be complicated by having a “middleware” service layer (e.g. Node.js or Ruby talking to a Java-based “true” backend).

Getting back to Remote Control library design: to make up for the non-synchronous nature of execution over the network, I ended up relying on Promises as return values for any of those client-side bridge wrapper functions. With newer ES features like async/await this will look less cumbersome than it may seem.

In summary, the client code could just “require” a server handle object, and then call methods on it as usual. Those would “call through” to a corresponding object instance on the server. Results were returned via a Promise back to the client code. That’s it: an almost completely transparent network layer. Specific examples are on the project’s GitHub page.

There are significant hurdles with using WebSockets as a communication layer, of course. Concurrent connection load is a concern. Restarting the link on network timeouts is cumbersome (and not yet implemented in Remote Control as a proof of concept). Frameworks like Meteor still make it work. As for me, my main intent was just to get out of the AJAX comfort zone and to ditch XHR baggage when exploring a pure-RPC approach.

Remote Control is still my current go-to library for any quick prototyping I need to do on my side projects. It turned out to be really good at getting an interlinked frontend and backend spun up quickly and with no hassle. It would be quite inadvisable to use it for production deployments, but I am happy with the use I found for it in quick proof-of-concept hacks.

Going forward, especially in the context of larger UI codebases, I think it would make sense to explore combinations of a data-driven query language like Facebook’s GraphQL with an RPC method set for mutating service behaviour calls. I believe in benefits of CQRS, and so it feels like the sweet spot is in adapting a nimble read-only data flow for presentation concerns juxtaposed against a strict and imperative API for making consistent state changes.

Either way, REST should not be treated as a “golden hammer” to apply to all situations indiscriminately.