Michal Witkowski is Improbable’s Principal Tech Lead on SpatialOS andMarcus Longmuir is the Tech Lead of Improbable’s Webtools Team.
REST+JSON is the de-facto standard way of building interactions between Web Apps and API servers. However, once you get past the initial ease of the prototyping phase, it shows problems with maintaining hand-crafted client code, debugging network protocol issues and lack of type safety. This blog post presents how we expanded our use of gRPC, the lingua franca for our microservice and client libraries, towards use in browser Web Apps. We’ll also show how this move, together with our adoption of TypeScript, made us end up in type-safe Web App Nirvana.
At Improbable we’re building SpatialOS, a PaaS offering for spatial-aware simulations that leverage existing simulation models, game engines, and visualisation clients written in multiple languages. Early on in the development of our platform, we decided on the importance of strongly typed, well documented APIs. Initially we went with REST and HAL, but the burden of re-implementing and maintaining clients for the plethora of languages (C++, Java, Go, C#, Objective-C) started to eat into our time to do what we really like, solving the actual problems our customers have, and we decided to look for something else.
At roughly the same time, Google open-sourced gRPC, the next-generation of their in-house RPC library based on the then brand-new HTTP2 protocol with support for code-generating clients and server stubs in multiple languages (C++, Java, Go, C#, Objective-C, NodeJS). The latter fit our bill perfectly, and after initial spike and validation we decided to adopt it company-wide. Quickly the .proto definitions, which provide the schema for gRPC services, became the standard way of thinking about services: they’re the first thing we converge on in design-discussions and the canonical documentation of how things work.
This has allowed teams with different areas of expertise (SDK, backend, CLI) and different language backgrounds to have a single way of expressing contracts and expectations. By leveraging the protocol buffers backwards and forwards compatibility, the question of “Do I need to add this field or not?” or “What does this field accept?” stopped coming up.
Since there was no support for using gRPC from a browser, the Web Apps teams were an exception to this culture shift. At the same time, they were meant to consume the same gRPC platform APIs as our clients and CLI tools, so we needed to find a way to expose them.
Fortunately, with gRPC REST Gateway, there exists a code-generator of REST+JSON API bindings for gRPC apis based on .proto file annotations. The approach we took was very similar to the one described here.
While this solution saved us from having to hand-write API-server-side code, it still had numerous drawbacks. Our Web App teams still needed to write JSON parsing code and client-side object representations, write the HTTP client libraries with the right calling conventions, and occasionally peek into the Network Console of the browser to figure out an odd networking error. But more importantly: new APIs (methods, fields) were only visible to the Web Apps when the platform API was regenerated and re-deployed alongside the gRPC REST Gateway. This was a huge obstacle for an engineering organisation that does independent and frequent rollouts.
We started thinking about eliminating the gRPC REST Gateway entirely from our API stack. It crucially does two things: translates protobuf messages to JSON and exposes an HTTP/1.1-compatible API with optional RESTful semantics.
That got us thinking: modern browsers are pretty modern. Surely they can deal with raw bytes of protobuf and not only JSON, and they do speak HTTP2. Why not make the browser speak the protobuf binary, and HTTP2-based gRPC itself?
We later learned that Google’s gRPC Team was working on a gRPC-Web spec internally. Their approach was eerily similar to ours, and we decided to contribute our experiences to the upstream gRPC-Web spec (currently in early access mode, still subject to change).
In the meantime, we rebased all of our Web Apps on TypeScript. Even though the initial spikes confirmed the value of the effort, it was only after we completed the migration that it became apparent how much we underestimated the long-term productivity boost that compile-time type validation will bring to our Web App teams.
So at this point we were ready to pull the trigger on productionizing our prototype. The goals were simple: make it easy to consume gRPC APIs (primarily written in Go) in a modern type-safe Web App (primarily in TypeScript).
We built a production-ready implementation of four components that are necessary to pull this off:
With these components, the .proto definitions of our services automatically generate the TypeScript objects needed to use our platform APIs, with all the TypeScript type-safety glory preserved from the .proto files. Moreover, the grpc-web-client is meant to be backwards compatible. Not only does it support non-Fetch API browsers, but also falls back to HTTP1.1 if needed. Currently our test matrix goes all the way back to:
The above diagram is a bit hard to grasp from an end-developer perspective, so here’s an extract from the example that we provide that will hopefully produce the aha! response.
Let’s say you use the following .proto file to define one RPC and one server-streaming RPCs for a hypothetical BookService.
After code-generating the Go server-side interfaces using golang/protobuf, you’d provide an example implementation:
Even though our motivation for gRPC-Web were primarily driven by the desire to eliminate the need for constantly rebuilding and redeploying gRPC REST Gateway, we think we have stumbled upon a potential game-changer for Web App development. Our experience of using gRPC Web has been transformative for our Web App teams:
In short, gRPC Web moves the interaction between frontend code and microservices from the sphere of hand-crafted HTTP requests to well-defined user-logic methods. This has been a massive boon to our Web App team’s productivity: they can focus on building the valuable client-side logic rather than hand-cranking REST clients.
To recognize how much Open Source technologies contributed to the success of Improbable, we decided to go beyond contributing back with bug reports and patches. Improbable Engineering now hosts a GitHub organisation improbable-eng where we will be open sourcing interesting pieces of our tech. improbable-eng/grpc-web is just one of them.
It is worth noting improbable-eng/grpc-web is not an official implementation. However, we look forward to working with the gRPC Team under the umbrella of Cloud Native Computing Foundation (CNCF) and are looking forward to contribute the TypeScript protocol buffers plugin, the TypeScript gRPC Client Library and the Go middleware upstream.
Sounds interesting? Check out our current Engineering roles including the Web and Infrastructure teams that built out gRPC-Web libraries.