DEV Community

sdcaulley
sdcaulley

Posted on • Edited on

Creating a gRPC Client as a Local npm Package

Making a local npm package is relatively easy - you create a new Node.js project.

npm init
Enter fullscreen mode Exit fullscreen mode

The sole purpose of this package is to have one client for each service that can be easily imported and used by other services. It uses the protobuf packages I created earlier.

I made the entry point to the package <name_of_service>.js, install the protobuf package needed, and gRPC:

npm install --save <path_to_package> @grpc/grpc-js
Enter fullscreen mode Exit fullscreen mode

I then set up my file:

const grpc = require('@grpc/grpc-js');
const { 
  <name_of_service>Services,
  <name of message>Messages } = require('<name_of_package');
const packageDefinition = grpc.loadPackageDefinition(<name_of_service>Services);
const meta = new grpc.Metadata();
Enter fullscreen mode Exit fullscreen mode

gRPC now knows to use the generated package definition, and which messages I need to use for this client.

Note on Messages
When using the generated files, you have to explicitly set up the messages. I did not have to do this originally using the raw *.proto files.

I also set up the metadata object that will be filled as needed by each operation.

I then set up a class that will be the export of the file:

class <name_of_service>Client {
  constructor(pathToService) {
    this.client = new packageDefinition.<name_of_service>(
      pathToService,
      grpc.credentials.createInsecure()
    );
  }
...
Enter fullscreen mode Exit fullscreen mode

I use the constructor to import the path to the service that the client is for, and then actually instantiate the client. I also use it to import any global variables needed, typically any metadata that needs to be sent with the request.

I then write any logic needed. Because these clients are to be "universal", I keep it very simple.

I create a helper function for creating the messages needed for each operation.

async createRequestMessage(request, type) {
  let message;

  switch (type) {
    case <type>:
    message = new <name of message>Messages.<message>();
    message.set<field>(request.<field>);
  }
  return message;
}
Enter fullscreen mode Exit fullscreen mode

The generated files creates a constructor class for each message it finds. So I start by instantiating a new instance of that class and using the .set* syntax to actually put my data in the class. This gets interesting with some of the field types, which I will discuss in later posts.

I then create an async function for each operation in the service.

...
async nameOfOperation(request) {
  meta.set('<key>', <value>);
  const messsage = await this.createRequestMessage(request, <type>);
  const promise = new Promise((resolve, reject) => {
      this.client.<name_of_operation>(message, meta, (err, res) => {
        if (err) {
          reject(err);
        } else {
          resolve(res);
        }
      });
    });

    return promise.then(value => {
      return value;
    });
  }
Enter fullscreen mode Exit fullscreen mode
  • I start by setting any metadata needed by the receiving service.
  • Then I send my request off to be turned into a gRPC approved message.
  • I then create an explicit Promise to receive the response from the service. Since gRPC is synchronous by nature, I found this approach works the best.
  • Once the Promise as been resolved or rejected, I return the value to the function that originally called it.

And then I export the class.

module.exports = <name_of_class>;
Enter fullscreen mode Exit fullscreen mode

I like to use the module.exports syntax over export default because it is explicit and easy for another developer to follow.

Because the clients are being used by multiple services for various reasons, it made sense to me to have them simply receive the request and return the response, thus allowing the service do what it needed to do with the response.

My clients can now be imported as npm packages to other services and used.

Top comments (0)