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.
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
2- Initialize typescript with your favorite config:
tsc init
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
}
}
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/
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;
}
}
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;
}
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
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
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
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
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
Now you need to run the generate command inside /proto
directory to generate code from proto buffers:
$ buf generate
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
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');
});
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);
};
...
Now we have to add the service to server:
...
import {HelloServiceService} from '../proto/services/hello/v1/hello_service_grpc_pb';
...
server.addService(HelloServiceService, {greet});
...
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');
});
Now we can add nodemon
to run our server and update it on change:
$ npm i nodemon
or
$ yarn add nodemon
And run the following command to start the server:
nodemon src/server/index.ts --watch /src/server
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());
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);
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());
});
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());
});
Run your client using the following command:
$ nodemon src/client/index.ts --watch src/client
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
-
@AriaAzadiPour
on Twitter
Top comments (16)
If you having issues (like below) running the
buf generate
command.Use the protobuf-es.
Replace the content of your
buf.gen.yaml
file withThen run
npx buf generate
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?
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.
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 errorFailure: plugin ts: could not find protoc plugin for name ts
running
buf generate --debug
gave this errorFailure: plugin grpc: exec: "grpc_tools_node_protoc_plugin": executable file not found in $PATH
and after installing globally buf generate works
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
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 .."
},
To get
npm run start
to work I had to also install ts-node like thisnpm i --dev ts-node
After
buf generare
got this errorMaybe 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
figured it out?
Do you know what "/" (slash) means?
If you are reffering to the text like
/src
,/
means the root directory of the project.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:
should read:
proto/
: proto buffers folder(I will explain more later)src/
: the source directorysrc/server/
: server directorysrc/client/
: client directorysrc/proto/
: auto generated code from proto buffersYes, but when used in this manner it is generally regarded as the root of the project.