DEV Community

Cover image for Use gRPC with Node.js and Typescript
Aria Azadi Pour
Aria Azadi Pour

Posted on • Updated on

Use gRPC with Node.js and Typescript

gRPC is a modern open-source high-performance Remote Procedure Call (RPC) framework that can run in any environment. And in this article, I am going to teach you how you can use gRPC to create high-performance RPC apps using node.js and typescript.

What is gRPC?

gRPC is a technology developed at Google in 2015. It is an RPC framework that will help you create RPC applications in many of your favorite languages. If you don't know what RPC is don't worry I'm going to explain it soon. This technology is used by google itself too. It is used quite a lot with microservice structures. according to Evaluating Performance of REST vs. gRPC from Ruwan Fernando gRPC is roughly 7 times faster than REST when receiving data and roughly 10 times faster than REST when sending data in the case he tested.

gRPC logo

What is RPC?

RPC is when a computer calls a procedure to execute in another address space. It is like calling another program to run action as it was ran on your computer and because of this, the request can be so much faster than REST.

Now lets go and create a simple application for sending hello messages.

Setup Project.

1- Initialize your project:

mkdir grpc-starter
cd grpc-starter
npm init -y
Enter fullscreen mode Exit fullscreen mode

2- Initialize typescript with your favorite config:

tsc init
Enter fullscreen mode Exit fullscreen mode

I use the following as my typescript configuration in the tsconfig.json file. you can use whatever matches your need the best

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": [
      "es6"
    ],
    "allowJs": true,
    "outDir": "build",
    "rootDir": "src",
    "strict": true,
    "noImplicitAny": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}
Enter fullscreen mode Exit fullscreen mode

3- create the folder structure:

  • /proto: proto buffers folder(I will explain more later)
  • /src: the source directory
  • /src/server: server directory
  • /src/client: client directory
  • /src/proto: auto generated code from proto buffers
grpc-starter/
├── proto/
└── src/
    ├── client/
    ├── proto/
    └── server/
Enter fullscreen mode Exit fullscreen mode

There are two ways to work with proto buffers and code generation in gRPC; dynamic or static. In static, we will generate types and code from our proto buffers but in dynamic we will not generate any typings from proto buffers and will use the code instead. dynamic can be a pretty good option if we were using JavaScript but since we need the typings to make our work easier while using TypeScript we will use the static way.

Create Proto Buffers

Proto Buffers are a way to serialize data. You may be very familiar with some other serialization languages like JSON and XML. Proto Buffers are just like them and it is developed by Google and wildly used with gRPC. In this article I'm not going to talk more about them, that's for another article.

First, we need to create the language enum. Well, you need to know a bit about folder structure in proto buffers we will create the language enum in /proto/com/language/v1/language.proto this is a package style folder structure that is necessary while using proto buffers with gRPC.

// /proto/com/language/v1/language.proto
syntax = "proto3";

package com.language.v1;

message Language {
  enum Code {
    CODE_UNSPECIFIED = 0;
    CODE_EN = 1;
    CODE_FA = 2;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we have to create our hello service in /proto/services/hello/v1/hello_service.proto.

// /proto/services/hello/v1/hello_service.proto
syntax = "proto3";

import "com/language/v1/language.proto";

package services.hello.v1;

service HelloService {
  rpc Greet(GreetRequest) returns (GreetResponse) {}
}

message GreetRequest {
  string name = 1;
  com.language.v1.Language.Code language_code = 2;
}

message GreetResponse {
  string greeting = 1;
  reserved "language_code";
  reserved 2;
}
Enter fullscreen mode Exit fullscreen mode

Buf

We will use a tool call Buf that will make code generation way easier for us. Check out the installation page to understand how you can install Buf.

Now we need to generate our buf config file at /proto/buf.yaml

# /proto/buf.yaml
version: v1beta1
build:
  roots:
    - .
lint:
  use:
    - DEFAULT
breaking:
  use:
    - WIRE_JSON
Enter fullscreen mode Exit fullscreen mode

The v1 directory that we have in our folder structure is because of the linting setting that we are using you can default the linting setting and use a different folder structure if you wish. The linting structure has also affected some of my code that you can check in Buf Docs.

Now you can run the commands below in /proto directory to check your code:

$ buf ls-files
com\language\v1\language.proto
services\hello\v1\hello_service.proto
Enter fullscreen mode Exit fullscreen mode

You can check your code for linting errors too. And if your proto buffers don't have any problem the command will return empty:

$ buf lint
Enter fullscreen mode Exit fullscreen mode

If you have used the code provided by me and your buf version is 1.0.0-rc1 your lint command should return no error.

Generating code

Well for code generation you can use protoc as it's the more popular tool but working with protoc is exhausting so we are going to use buf.

Now you need to generate the buf generation config at /proto/buf.gen.yaml:

# /proto/buf.gen.yaml
version: v1beta1
plugins:
  - name: js
    out: ../src/proto
    opt: import_style=commonjs,binary
  - name: grpc
    out: ../src/proto
    opt: grpc_js
    path: grpc_tools_node_protoc_plugin
  - name: ts
    out: ../src/proto
    opt: grpc_js
Enter fullscreen mode Exit fullscreen mode

Now you have to install grpc-tools and grpc_tools_node_protoc_ts using npm or yarn. These two package will help us generate code for TypeScript using buf:

$ npm i -D grpc-tools grpc_tools_node_protoc_ts
or
$ yarn add -D grpc-tools grpc_tools_node_protoc_ts
Enter fullscreen mode Exit fullscreen mode

Now you need to run the generate command inside /proto directory to generate code from proto buffers:

$ buf generate
Enter fullscreen mode Exit fullscreen mode

Implement the server

First thing we need to do is to add the gRPC package to create our server:

$ npm i @grpc/grpc-js
or
$ yarn add @grpc/grpc-js
Enter fullscreen mode Exit fullscreen mode

Now create the /src/server/index.ts file and start the gRPC using the code below:

import {
    Server,
    ServerCredentials,
} from '@grpc/grpc-js';
const server = new Server();

server.bindAsync('0.0.0.0:4000', ServerCredentials.createInsecure(), () => {
    server.start();

    console.log('server is running on 0.0.0.0:4000');
});
Enter fullscreen mode Exit fullscreen mode

Using this code we can create a new server and bind it to 0.0.0.0:4000 which is like starting an express server at port 4000.

Now we can take advantage of our statically generated code to create a typed Greet handler like below:

import {
    ServerUnaryCall,
    sendUnaryData,
    Server,
    ServerCredentials,
} from '@grpc/grpc-js';

import {Language} from '../proto/com/language/v1/language_pb';
import {
    GreetRequest,
    GreetResponse,
} from '../proto/services/hello/v1/hello_service_pb';

const greet = (
    call: ServerUnaryCall<GreetRequest, GreetResponse>,
    callback: sendUnaryData<GreetResponse>
) => {
    const response = new GreetResponse();

    switch (call.request.getLanguageCode()) {
        case Language.Code.CODE_FA:
            response.setGreeting(`سلام، ${call.request.getName()}`);
            break;
        case Language.Code.CODE_UNSPECIFIED:
        case Language.Code.CODE_EN:
        default:
            response.setGreeting(`Hello, ${call.request.getName()}`);
    }

    callback(null, response);
};

...
Enter fullscreen mode Exit fullscreen mode

Now we have to add the service to server:

...

import {HelloServiceService} from '../proto/services/hello/v1/hello_service_grpc_pb';

...

server.addService(HelloServiceService, {greet});

...
Enter fullscreen mode Exit fullscreen mode

At the end your server file should look like something like this:

import {
    ServerUnaryCall,
    sendUnaryData,
    Server,
    ServerCredentials,
} from '@grpc/grpc-js';

import {Language} from '../proto/com/language/v1/language_pb';
import {
    GreetRequest,
    GreetResponse,
} from '../proto/services/hello/v1/hello_service_pb';
import {HelloServiceService} from '../proto/services/hello/v1/hello_service_grpc_pb';

const greet = (
    call: ServerUnaryCall<GreetRequest, GreetResponse>,
    callback: sendUnaryData<GreetResponse>
) => {
    const response = new GreetResponse();

    switch (call.request.getLanguageCode()) {
        case Language.Code.CODE_FA:
            response.setGreeting(`سلام، ${call.request.getName()}`);
            break;
        case Language.Code.CODE_UNSPECIFIED:
        case Language.Code.CODE_EN:
        default:
            response.setGreeting(`Hello, ${call.request.getName()}`);
    }

    callback(null, response);
};

const server = new Server();

server.addService(HelloServiceService, {greet});

server.bindAsync('0.0.0.0:4000', ServerCredentials.createInsecure(), () => {
    server.start();

    console.log('server is running on 0.0.0.0:4000');
});
Enter fullscreen mode Exit fullscreen mode

Now we can add nodemon to run our server and update it on change:

$ npm i nodemon
or
$ yarn add nodemon
Enter fullscreen mode Exit fullscreen mode

And run the following command to start the server:

nodemon src/server/index.ts --watch /src/server
Enter fullscreen mode Exit fullscreen mode

Now that we have our server ready let's go and create our client.

Implement the client

Create the /src/client/index.ts file to start writing the client code.

In the client first we need to connect to our service client using the code below:

import {credentials} from '@grpc/grpc-js';

import {HelloServiceClient} from '../proto/services/hello/v1/hello_service_grpc_pb';

const client = new HelloServiceClient('localhost:4000', credentials.createInsecure());
Enter fullscreen mode Exit fullscreen mode

Now we can create the request and populate it with our values like below:

...

import {Language} from '../proto/com/language/v1/language_pb';
import {GreetRequest} from '../proto/services/hello/v1/hello_service_pb';

...

const request = new GreetRequest();

request.setName('Aria');
request.setLanguageCode(Language.Code.CODE_EN);
Enter fullscreen mode Exit fullscreen mode

At the end you can send the request and receive the response:

...

client.greet(request, (error, response) => {
    if (error) {
        console.error(error);

        process.exit(1);
    }

    console.info(response.getGreeting());
});
Enter fullscreen mode Exit fullscreen mode

Your client file should look like this:

import {credentials} from '@grpc/grpc-js';

import {Language} from '../proto/com/language/v1/language_pb';
import {HelloServiceClient} from '../proto/services/hello/v1/hello_service_grpc_pb';
import {GreetRequest} from '../proto/services/hello/v1/hello_service_pb';

const client = new HelloServiceClient(
    'localhost:4000',
    credentials.createInsecure()
);

const request = new GreetRequest();

request.setName('Aria');
request.setLanguageCode(Language.Code.CODE_EN);

client.greet(request, (error, response) => {
    if (error) {
        console.error(error);

        process.exit(1);
    }

    console.info(response.getGreeting());
});
Enter fullscreen mode Exit fullscreen mode

Run your client using the following command:

$ nodemon src/client/index.ts --watch src/client
Enter fullscreen mode Exit fullscreen mode

Final words

Huge shoutout to Slavo Vojacek for his article on handling the proto buffers for typescript that has helped this article a lot.

You can check out the full repository at my GitHub repo

While gRPC is amazing and super fast but it is not the best practice to use it for freelancing projects and small projects cause it will cost you a lot of time compared to REST but if you are building a dream and you want it to be the best you can have gRPC as an option and think if it is worth the cost.

Resources

Find Me

Top comments (13)

Collapse
 
ukor profile image
Ukor Jidechi E.

If you having issues (like below) running the buf generate command.

Failure: plugin js: js moved to a separate plugin hosted at https://github.com/protocolbuffers/protobuf-javascript in v21, you must install this plugin
Enter fullscreen mode Exit fullscreen mode

Use the protobuf-es.

  • Install the dependency
npm install @bufbuild/protobuf @bufbuild/protoc-gen-es @bufbuild/buf
Enter fullscreen mode Exit fullscreen mode

Replace the content of your buf.gen.yaml file with

version: v1
plugins:
  - plugin: es
    opt: target=ts
    out: ../src/proto

Enter fullscreen mode Exit fullscreen mode

Then run npx buf generate

Collapse
 
adokhugi profile image
Claus Volko

Excuse me, but this fails to generate the *.ts and *_grpc_pb.ts files. How do I have to modify buf.gen.yaml to get all files?

Collapse
 
adokhugi profile image
Claus Volko

I noticed that if I do generate the _grpc_pb.js file (js instead of ts), I get the error:

SyntaxError: The requested module './proto/octants/v1/octants_grpc_pb' does not provide an export named 'DownloadOctantsService'

In my program this DownloadOctantsService is located in the _grpc_pb.js file and it is there, so apparently the reason for the error message is an incompatibility between JavaScript and TypeScript. Unfortunately translating the js file to ts using ChatGPT did not help since in that case, some methods referenced in the file are not found.

Collapse
 
jkristia profile image
Jesper Kristiansen

Great article. There are very few articles on typescript with gRPC.
I had to install grpc_tools_node_protoc_ts globally. Without it I got this error
Failure: plugin ts: could not find protoc plugin for name ts

running buf generate --debug gave this error

Failure: plugin grpc: exec: "grpc_tools_node_protoc_plugin": executable file not found in $PATH

and after installing globally buf generate works

Collapse
 
lorefnon profile image
Lorefnon

If locally installed, node_modules/.bin has to be in path.

If instead of invoking it directly, we add an npm script to invoke it, then that will be taken care of by npm/yarn.

In package.json

{ 
    "scripts":  {
        "codegen:buf": "buf generate"
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rawnsley profile image
Rupert Rawnsley

Thanks for this tip. Didn't quite work for me until I copied the way the line above was structured:

"scripts": {
"start": "nodemon src/server/index.ts --watch src/server",
"start:client": "nodemon src/client/index.ts --watch src/client",
"proto:build": "cd proto; buf build; cd ..",
"codegen:buf": "cd proto; buf generate; cd .."
},

Collapse
 
rawnsley profile image
Rupert Rawnsley

To get npm run start to work I had to also install ts-node like this npm i --dev ts-node

Collapse
 
andriivyn profile image
Andriivyn

After buf generare got this error
Maybe someone could help?

Failure: plugin js: js moved to a separate plugin hosted at https://github.com/protocolbuffers/protobuf-javascript in v21, you must install this plugin; js moved to a separate plugin hosted at https://github.com/protocolbuffers/protobuf-javascript in v21, you must install this plugin

Collapse
 
blonteractor profile image
Blonteractor

figured it out?

Collapse
 
onkeltem profile image
Artiom Neganov

Do you know what "/" (slash) means?

Collapse
 
devaddict profile image
Aria Azadi Pour

If you are reffering to the text like /src, / means the root directory of the project.

Collapse
 
onkeltem profile image
Artiom Neganov

Ah, that's the source of the confusion.
/ never actually means the root of a project. It's always the root of the filesystem.

E.g. this:

/proto: proto buffers folder(I will explain more later)
/src: the source directory
/src/server: server directory
/src/client: client directory
/src/proto: auto generated code from proto buffers

should read:

proto/: proto buffers folder(I will explain more later)
src/: the source directory
src/server/: server directory
src/client/: client directory
src/proto/: auto generated code from proto buffers

Thread Thread
 
devaddict profile image
Aria Azadi Pour

Yes, but when used in this manner it is generally regarded as the root of the project.