Michal Witkowski is Improbable’s Principal Tech Lead on SpatialOS and Marcus 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.
Sidenote: why gRPC in the first place?
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.
Web was the exception
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.
The hacky and not-so-hacky prototypes
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?
So as the not-so sunny London summer of 2016 was rolling to a close, we decided to spike things out: using the official proto3 JavaScript codegen, a neat hack on the Fetch API, and a bit of Go-side HTTP2-hackery… we made a browser speak gRPC to a Go-based gRPC server.
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.
gRPC-Web for TypeScript and Go
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:
- grpcweb – a Go package that wraps an existing Go gRPC server and makes it into gRPC-Web
- grpcwebproxy – a gRPC-Web stand-alone reverse proxy for classic gRPC servers (e.g. in Java or C++)
- ts-protoc-gen – a TypeScript plugin for the protocol buffers compiler (protoc) that produces TypeScript service definitions and TypeScript declarations for the standard JavaScript objects generated by upstream protoc.
- grpc-web-client – a TypeScript gRPC-Web client library for browsers that abstracts away the networking (Fetch API or XHR) from users and codegenerated classes
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:
- IE 10+
- Edge 13+
- Firefox 38+
- Chrome 41+
- Safari 8+
Paint me a code picture
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.
syntax = “proto3”;
package examplecom.library;
message Book {
int64 isbn = 1;
string title = 2;
string author = 3;
}
message GetBookRequest {
int64 isbn = 1;
}
message QueryBooksRequest {
string author_prefix = 1;
}
service BookService {
rpc GetBook(GetBookRequest) returns (Book) {}
rpc QueryBooks(QueryBooksRequest) returns (stream Book) {}
}
After code-generating the Go server-side interfaces using golang/protobuf, you’d provide an example implementation:
import pb_library “../_proto/examplecom/library“
type bookService struct{
books []*pb_library.Book
}
func (s *bookService) GetBook(ctx context.Context, bookQuery *pb_library.GetBookRequest) (*pb_library.Book, error) {
for _, book := range s.books {
if book.Isbn == bookQuery.Isbn {
return book, nil
}
}
return nil, grpc.Errorf(codes.NotFound, “Book could not be found“)
}
func (s *bookService) QueryBooks(bookQuery *pb_library.QueryBooksRequest, stream pb_library.BookService_QueryBooksServer) error {
for _, book := range s.books {
if strings.HasPrefix(s.book.Author, bookQuery.AuthorPrefix) {
stream.Send(book)
}
}
return nil
}
After code-generating the JavaScript/TypeScript messages and method stubs you would invoke the code from the browser using the following example snippet of code:
import {grpc, BrowserHeaders} from “grpc-web-client“;
// Import code-generated data structures.
import {BookService} from “../_proto/examplecom/library/book_service_pb_service“;
import {QueryBooksRequest, Book, GetBookRequest} from “../_proto/examplecom/library/book_service_pb“;
const queryBooksRequest = new QueryBooksRequest();
queryBooksRequest.setAuthorPrefix(“Geor“);
grpc.invoke(BookService.QueryBooks, {
request: queryBooksRequest,
host: “https://my.grpc.server.example.com“,
onMessage: (message: Book) => {
console.log(“got book: “, message.toObject());
},
onEnd: (code: grpc.Code, msg: string | undefined, trailers: BrowserHeaders) => {
if code == grpc.Code.OK {
console.log(“all ok“)
} else {
console.log(“hit an error“, code, msg, trailers);
}
}
});
The future of Web App development
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:
- No more hunting down API documentation – .proto is the canonical format for API contracts, just like in other teams
- No more hand-crafted JSON call objects – all requests and responses are strongly typed and code-generated, with hints available in the IDE
- No more dealing with methods, headers, body and low level networking – everything is handled and abstracted away in the grpc-web-client
- No more second-guessing the meaning of HTTP error codes – gRPC status codes are a canonical way of representing issues in APIs
- No more hand-crafted chunk-encoded streaming madness on the server – gRPC-Web supports both 1:1 RPCs and 1:many server-side streaming requests
- No more data parse errors when rolling out new binaries – backwards and forwards-compatibility of requests and responses is guaranteed by protocol buffers
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.
Open Source and Contributing
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.
We make SpatialOS – find out more about our platform and download our SDK for games development.