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.

Screenshot of protoc

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.

Screenshot of node protoc

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

👋🏼

Hey, I'm Pierce! I write mostly about engineering, machine learning, and building companies. 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. Promise.

Debugging chrome extensions with system-level logging
Extensions are basically mini web applications these days, just with access to a `chrome` global variable that can interact with some browser-level functionality. Aside from that - it's all familiar. That extends to the debugging experience. Since extensions run in the regular V8 Chrome runtime, Chrome exposes the same debugging tools that you're used to on the web.
The curious case of LM repetition
I was doing some OSS benchmarking over the weekend and was running into an odd issue. Some families of models would respond with near-gibberish, even with straightforward prompt inputs. This is a debugging session for LLM repetition.
Headfull browsers beat headless
Twenty years ago a simple curl would open up the world. HTML markup was largely hand designed so id and name attributes were easily interpretable and parsable. Now most sites render dynamic content or use template defined class tags to define the styling of the page. Building a headfull browser container to more easily deploy and debug Chromium in a remote cluster.