rhinodavid / grpc-promise-ts-example-app

A Typescript/gRPC example with Promises

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

A minimal command line app to serve as an example of how create a node Typescript app which uses gRPC with Promises

Screenshot of the joke command line app

And it tells you a joke.

This app uses two packages I've been working on:

grpc-promise-ts is used at runtime to convert the gRPC client stub generated by grpc-tools to a client with an ES6 Promise API. Server functionality is in the works. It is a fork of the fantastic grpc_tools_node_protoc_ts package.

grpc-promise-ts-generator-plugin is a plugin for grpc-tool's proto compiler used to generate Typescript definitions for Javascript proto implementations. Optionally, it can also generates Typescript definitions for the Promise clients grpc-promise-ts creates at runtime.

With both we have a type safe way to call:

const response = await service.getSomething(request);

Installing and running

git clone https://github.com/rhinodavid/grpc-promise-ts-example-app && \
cd grpc-promise-ts-example-app && yarn && yarn build && yarn start

How can I make an app like this?

A great place to start is the blog post gRPC with Node.js and TypeScript by @idnan. It's a fantastic introduction but doesn't include a client implementation.

If you need details check out that post. Here I'll give you a broad overview of the steps used to build this app.

1. Create the proto

The protocol buffer (commonly, "proto") joke.proto defines request and repsonse messages between the client and the server, as well as what remote procedure calls (RPCs) exist on the server.

2. Generate Javascript jspbs and Typescript definitions

Next up: turn those protos into something we can use in Node.

A portion of the script to generate Javascript and Typescript definitions is reproduced here:

PROTOC="./node_modules/.bin/grpc_tools_node_protoc"
NODE_PLUGIN="./node_modules/.bin/grpc_tools_node_protoc_plugin"
TYPESCRIPT_PLUGIN="./node_modules/.bin/grpc-tools-node-typescript-promise-plugin"

${PROTOC} \
  --js_out=import_style=commonjs,binary:"./${OUT_DIR}" \
  --plugin=protoc-gen-grpc="${NODE_PLUGIN}" \
  --grpc_out="./${OUT_DIR}" \
  --plugin=protoc-gen-tspromise="${TYPESCRIPT_PLUGIN}" \
  --tspromise_out=gen-promise-clients:"./${OUT_DIR}" \
  -I "${PROTO_DIR}" \
  "${PROTO_DIR}"/*.proto

This script is complex, so lets break it down.

First we define paths to symlinked binaries/scripts in our node_modules/.bin folder. grpc_tools_node_protoc and grpc_tools_node_protoc_plugin are installed by the grpc-tools package.

grpc_tools_node_protoc is a Javascript wrapper around the C++ protoc compiler which translates protos into language implementations. The Javascript implementation is jspb, whose output is configured by the --js_out flag. There's some mention in the docs of an ES6 output, but that doesn't appear to be implemented as of commit c649397 in mid-2020, so you'll want to stick with commonjs.

grpc_tools_node_protoc_plugin also ships with grpc-tools and generates Javascript server and client stubs for the services specified in your protos. These stubs allow you to write Javascript which communicates to the gRPC server and client binaries which do the actual communication.

grpc-tools-node-typescript-promise-plugin is installed by grpc-promise-ts and generates Typescript definitions for messages and services contained in the .proto files.

If you plan to use grpc-promise-ts to make Promise clients for your services, don't forget to include gen-promise-clients in the output configuration argument of the plugin.

Now you're ready to run the script, which you can do with yarn build-protos. Once it completes, the jspb folder will contain a .js implementation and a .d.ts Typescript definition for the proto message, plus a second pair for the services.

These generated files are commited to the repo for easy reference on Github, but I generally do not include generated files in repositories for actual projects.

In your proto output directory, if you don't have a file like joke_pb.js but you do have a file like jokerequest.js, make sure you have import_style=commonjs in the --js_out argument in the build script. You've built files with closure compiler imports, which nobody wants.

3. Create the handler for the Joke service

For each service added to the server you'll implement a handler. The handler contains a function for each RPC in the service which takes a request message and returns a response message.

Take a look at src/server/jokeHandler.ts. This handler implements getAJoke: (request: JokeRequest, response: JokeResponse) => void. First, it gets the type of joke the user wants from the request. Next, it gets a random joke of that type from the jokes it knows and adds it to the response. Finally, it waits for a bit and then calls the callback with the response.

4. Create the server

The server implementation is at src/server/server.ts. For each service you'd like the server to provide, call server.addService(<service>, <handler>) to add it to the server. Then bind the server to a host/port and start it.

5. Create the client

The client is created by a helper function in src/client/createJokePromiseClient.ts.

To create the callback client use new JokeClient("<host>:<port>", credentials.createInsecure()) (when you eventually add authentication to your server this will take a bit more configuration). The JokeClient constructor is generated by the build-protos script.

PROMISIFY IT

convertToPromiseClient from the grpc-promise-ts package creates a client with a Promise API (make sure you included gen-promise-clients in your proto build script):

const promiseClient = convertToPromiseClient(callbackClient);

Now we can await reponses to the RPCs!

Using the promise client

For unary RPCs (one request in, one response out) the signature is:

(request: TRequest, metadata?: Metadata, options?: Partial<CallOptions>) => TUnaryResult<TResponse>;

Let's look at TUnaryResult. Its signature is:

interface TUnaryResult<TResponse> extends Promise<TResponse> {
  getUnaryCall: () => grpc.ClientUnaryCall;
}

Since it extends Promise we can await its result like any other Promise. So getting a response looks like

const response = await promiseClient.getJoke(jokeRequest);

If you need access to the grpc.ClientUnaryCall you can remove the await and call getUnaryCall on the result:

const result = promiseClient.getJoke(jokeRequest);
const unaryCall = result.getUnaryCall();
try {
  const response = await result;
} catch (e) {
  console.error(`Promise rejected: ${e}`);
}
unaryCall.cancel(); // promise will reject if it was still pending

6. Putting it together

The joke CLI is implemented in src/app.ts.

Most real apps won't start the client and server from the same process.

The logic steps the app are:

  • Get a free port
  • Start the server
  • Start the client
  • Use Inquirer to ask the user what kind of joke they want
  • Build a request proto with that choice
  • Send the request via the client to the server and awaits the response
  • Show the response text to the user
  • Shut down the client
  • Shut down the server

Don't forget to shutdown your client, otherwise your node process will not exit (servers generally don't shut down).

About

A Typescript/gRPC example with Promises

License:MIT License


Languages

Language:JavaScript 58.5%Language:TypeScript 37.6%Language:Shell 3.9%