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.
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.
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.
-
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. ↢