How to implement a gRPC client and server in TypeScript
Edit: Since first publishing this story, the original Node grpc
library has been scheduled for deprecation, to be replaced by the new pure JS implementation @grpc/grpc-js
. Only minor changes to the implementation described in this post are required to support the new library, which are detailed in pull requests here and here.
TypeScript brings the myriad benefits of static typing to JavaScript. At Blokur, it allows us to catch errors early, write code more quickly and refactor confidently.
We also make extensive use of gRPC for service-to-service communication. Protocol buffer service definitions make it dead easy for developers to know how to communicate with a given service. With some careful setup, interfacing with a remote service can feel as simple as calling a local function.
It can, however, be a little fiddly to properly set up your TypeScript gRPC server/client implementation to properly use type definitions generated from your .proto files.
Without further ado, here’s how to achieve the perfect TypeScript + gRPC setup!
TL;DR: Go ahead and browse the source code of the demo project!
Let’s talk about Songs
Since Blokur is a music company, our demo application is going to be a CLI tool that allows you to discuss your favourite songs with your friends.
First, we’ll create our service definition in proto/songs.proto
.
We will implement each of the RPC call types:
GetSong
(unary). Returns a random song from the database.AddSongs
(client-side stream). Add multiple songs to the database.GetChat
(server-side stream). Returns the list of chat messages on a song.LiveChat
(bidirectional stream). Engage in live discussion about a song, simultaneously sending chat messages and receiving them from others.
With a bit of meta-programming magic, we can compile our protobuffer service definition into JavaScript and the corresponding TypeScript type definitions. We use this fantastic tool, which conveniently produces type definitions for the RPC methods on both the server and client.
First, let’s install the required dependencies:
yarn add grpc grpc-tools grpc_tools_node_protoc_ts
Next, let’s create a build script in scripts/build-protos.sh
:
Finally, we can run it with:
sh ./scripts/build-protos.sh ./songs.proto ./src/proto
Now you’ll find your generated code in src/proto
:
songs_grpc_pb.js
(JS code for interacting with RPC methods)songs_grpc_pb.d.ts
(… and associated type definitions)songs_pb.d.ts
(JS code for interacting with message definitions)songs_pb.js
(… and associated type definitions)
Implementing the Server
Our generated code exposes a convenient interface which describes precisely how we can implement the gRPC server.
It’s full typed: both callback- and stream-based handlers use generic types to describe precisely the format of the messages we are passing around (defined, of course, in our service definition).
Once we’ve implemented all our RPC handlers, we can start up the server with something like:
const server = new grpc.Server();server.addService<ISongsServer>(SongsService, new SongsServer());console.log(`Listening on ${process.env.PORT}`);server.bind(`localhost:${process.env.PORT}`, grpc.ServerCredentials.createInsecure());server.start();
Clean!
Implementing the Client
The generated code exposes a constructor to create a new gRPC client instance for our service:
import grpc from 'grpc';
import services from '../proto/songs_grpc_pb';export default new services.SongsClient(/* ...args */);
The returned instance satisfies the ISongsClient
interface, which again precisely describes how we can interact with our server implementation, and how to build the messages we will send over the wire. For example, here’s the implementation for calling the GetChat
endpoint:
Note how we can construct a Song
instance, set the appropriate properties with the exposed setter methods, and invoke getChat
to get access to a stream on which we will receive messages of typeComment
.
Watch the demo
Static typing is great not just because it allows you to catch common errors at build time (rather than runtime 😬), but because of the superior developer experience. Knowing the contracts your objects and functions are expected to adhere to inside your IDE just makes life so much easier.
For all the nitty-gritty details of how to achieve this TypeScript + gRPC integration, check out the demo repo. The demo wraps up all that we’ve discussed in a CLI application, which you can see in action in this demo video.