Using grpc with node and typescript

# February 16, 2023

Documentation for using grpc within node is split between static generation and dynamic generation. Dynamic generation compiles protobuffer definition files at runtime through protobuf.js and typically looks like the following:

var PROTO_PATH = __dirname + '/../../../protos/api.proto';
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var protoDescriptor = grpc.loadPackageDefinition(packageDefinition);

Most of the grpc docs use the dynamic approach - I assume for ease of getting started. The main pro to dynamic generation is faster prototyping if the underlying schema changes, since you can hot reload the server/client. But one key downside includes not being able to typehint anything during development or compilation. For production use compiling it down to static code is a must.

I've started using a pipeline that re-generates static compiled files and their typescript definitions automatically. Here's how to do it.

The standard protoc version that generates code for Go/Java/Python/etc. doesn't have support for generating javascript definition files.

Where's my javascript at??

JS generation is only supported through the grpc-tools library, which includes a wrapped version of protoc and a plugin to generate javascript code.1 The CLI interface is identical except now it offers javascript generation support.

Ah, there you are.

So to get started you'll have to download grpc-tools and grpc_tools_node_protoc_ts as dependencies. The second dependency will support the typescript generation.

npm install -d grpc-tools grpc_tools_node_protoc_ts

The javascript-enabled protoc now lives within node modules, so to make sure it installed correctly you can call:

$ ./node_modules/.bin/grpc_tools_node_protoc --help

Now it's time to wire everything up. Start with generating the javascript code. The --js_out argument will generate the definition files for messages, enums, and the like. The --grpc_out will generate the stub files for client and server implementations. In most applications you'll need both. If bash is your speed:

build.sh:

(
    mkdir -p app/src/api \
    && app/node_modules/.bin/grpc_tools_node_protoc \
        --grpc_out=grpc_js:app/src/api \
        --js_out=import_style=commonjs,binary:app/src/api \
        protos/api.proto
)

To generate the typescript definitions:

build.sh:

(
    protoc \
        --plugin=protoc-gen-ts=app/node_modules/.bin/protoc-gen-ts \
        --ts_out=grpc_js:app/src/api \
        protos/api.proto
)

At this point you should have a populated app/src/api/protos folder with all the definition files. To use in your main code, just import the appropriate classes and bask in the glow of your newfound typehints.

import { CommandMessage } from './api/protos/api_pb';
import { APIClient } from './api/protos/api_grpc_pb';

The one last step is to automate the rebuild process. I use chokidar to watch the protobuf directory and re-run this build script. I also add a final generation just for good measure before I build my typescript files down to javascript:

{
    ...
    "scripts": {
        "build": "./build.sh && tsc",
        "watch-protobuf": "./watch.js"
    }
}

Easy. And your dev workflow will thank you.


  1. It should be possible to use the plugin directly with the standard protoc install through the --plugin keyword arg, but it didn't seem to work as of libprotoc 3.21.8. 

Related tags:
#programming #webapp
Network routing interaction on MacOS
There are a series of resolution layers governing DNS, IP, and port routing on OSX. Included are notes on the different routing utilities supported locally, specifically using /etc/hosts, ifconfig, pfctl, and /etc/resolver.
Debugging slow pytorch training performance
A deep dive into debugging slow GPU utilization in a pytorch lightning training pipeline. Some tricks with SimpleProfiler and DatasetWrapper to help you debug your dataloader woes.
Building our home network
Notes on building our 10gbps home network, from choosing Sonic as our ISP to a blinking server rack in the corner.

Hi, I'm Pierce

I write mostly about engineering, machine learning, and company building. If you want to get updated about longer essays, subscribe here.

I hate spam so I keep these infrequent - once or twice a month, maximum.