How to implement a gRPC client and server in TypeScript

Mike Christensen
Blokur
Published in
4 min readFeb 19, 2020

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.

A fully-typed gRPC client provides a better development experience

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.

The callback function knows you need to pass it a Song

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:

getChat accepts a Song and returns a ClientReadableStream over which we will send Comment messages

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.

--

--