Your web application receives JSON data from the server and shows it in the UI. Simple, right? Indeed, but also filled with pitfalls. Mistyped a field name in UI code? UI crashes. Server leaves out a field you thought would always be there? UI crashes. You quickly become vigilant about checking that you actually received the data you expected to receive, but it never ceases being a burden.
I’m going to describe using gRPC, a cross-platform framework for APIs, to make sure the client and the server agree on message contents. This gives us several great features:
- Single source of truth. Each API is described in Protocol Buffers language, also known as a
.proto
file, and in that language only. From this description you can generate API code for just about any commonly used programming language out there. - Clear upgrade path. Protocol Buffer messages have clearly defined semantics for common changes like adding and removing fields from messages. Very often you can just update message description and old clients work seamlessly with new servers and old servers work with new clients as well. When the API changes are too large, it’s simple to do a versioned API.
- Cross-platform. A single server implementation can easily talk to a Typescript web app, an Android app, an iOS app and more.
- Compact on-wire binary format. OK, this is only partially true – a lot of what I’ll describe will use JSON as the wire format between the client and the server. But typically gRPC would use the Protocol Buffer binary format that is often significantly smaller than JSON.
What is gRPC?
The gRPC is a cross-platform framework for communicating between different pieces of software over network. It was originally developed by Google and is extensively used within Google, but is not in any way tied to them. You might think the name is related to the origins, but it obviously is just a recursive acronym for “gRPC Remote Procedure Calls”. It’s closely related to Protocol Buffers, a domain-specific language for describing messages and a binary format for storing these messages.
In a typical application, a gRPC server receives and transmits messages in Protocol Buffers binary format over a HTTP/2 channel. This works nicely for server-to-server communications and for Android, iOS, Windows, Mac, Linux, etc. applications.
For clients running inside a web browser, however, a different approach must be taken. While modern browsers can talk HTTP/2, a gRPC implementation would require a low-level access to the HTTP/2 connection, and no browser provides that and likely never will. Instead we’ll need to use some sort of a translation layer. This can either be a separate proxy server, or built-in to the application server. With this layer, the browsers can do HTTP requests as usual and the data can be transmitted either in the Protocol Buffers binary format or in the JSON format.
In browser applications, using JSON as wire format gives certain advantages. Notably the in-browser developer tools automatically pretty-print JSON messages for you, whereas it’s practically impossible to read a Protocol Buffers message in the network traffic view. This is not an all-or-nothing choice, though, since HTTP content negotiation features make it possible to write servers and clients that can use either format. For example, debug version of your app could use JSON and production version Protocol Buffers.
Typescript for write-time type checking
Typescript, an extended version of Javascript developed by Microsoft, is the modern approach in web application development to giving types to variables and for doing compile-time type checking. Not the only one, mind – for example, Google separately developed their own way of attaching type info to Javascript that used type declarations in specially formatted code comments.
Typescript helps somewhat with handling API message contents. If you manually create a type definition for the data you expect to receive, your IDE can auto-complete field names for you and immediately warn you for mistyped field names and for assuming a wrong type for a field. If your backend is also written in Typescript, you can even share the same type declaration between the server and the client. With that, you could already be fairly certain that the messages sent by the server match the ones the clients expect to receive.
It is possible to generate Typescript API interfaces and message type declarations from protocol buffer files. With this approach, you only need to declare the message types once instead of writing them separately for each programming language you use. This is especially important when modifying the message contents, such as adding new fields. If you need to update several declarations separately they are likely to diverge, but a single declaration will not.
The gRPC client libraries don’t just give compile-time type checks, but also runtime assistance. Say, what happens when you leave a field unset on the server and send the message as JSON? Depending on your JSON library and its settings, typically the field would either get left out of the message altogether or it would get set to null. The gRPC client libraries, however, ensure that the runtime object decoded from the JSON contains all the fields that were declared in the proto file at the compile time and that they are of the correct type.
Coming up
In future posts I’m going to delve into the details on how exactly the above gets done. Especially I will look into building a C#/.NET server that talks with a Typescript/React application that uses Yarn as package manager. These topics will include:
- How to automatically compile
.proto
files- Into Typescript code using Yarn
- Into a .NET package using
csproj
definition anddotnet
tools
- How to write a .NET single controller that talks to both native gRPC clients and to web clients
- How to call these gRPC services from a React app
Top comments (0)