Implementation of gRPC in web and server with TypeScript

Mikhail Gus'kov
Frontend Weekly
Published in
5 min readOct 21, 2022

This is a short guide on how to use GRPS in web projects. What components are needed and how to simplify your work. The article will provide the main points and approaches, more complete code and instructions for launching can be found in the git repository at the link at the end.

Let’s start, gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. With gRPC you can define your service once in a .proto file and implement clients and servers in any of gRPC’s supported languages.

Define the Service

The first step when creating a gRPC service is to define the service methods and their request and response message types using protocol buffers. In this example, we define our UserService in a file called users.proto. For more information about protocol buffers and proto3 syntax, please see the protobuf documentation. We will make a service in which users will be created, we will be able to get the user by id and the entire list through the stream.

syntax = "proto3";

package users;

import "google/protobuf/empty.proto";

enum UserStatus {
UNKNOWN = 0;
OFFLINE = 1;
BUSY = 2;
AVAILABLE = 3;
}

message User {
int32 id = 1;
string name = 2;
int32 age = 3;
UserStatus status = 4;
}

message UserRequest {
int32 id = 1;
}

service Users {
rpc GetUser(UserRequest) returns (User) {};
rpc CreateUsers(stream User) returns (google.protobuf.Empty) {}
rpc CreateUser(User) returns (google.protobuf.Empty) {}
rpc GetUsers(google.protobuf.Empty) returns (stream User) {};
}

Auto code generation

We can generate code from proto files both for the server and for the web application. For generating code for NodeJs there is grpc-tools for generate JS and grpc_tools_node_protoc_ts for generate TS which can be installed from NPM. The bash script will look like this:

#!/bin/bash

PROTO_DIR=./server/proto

# Generate JavaScript code
npx grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${PROTO_DIR} \
--grpc_out=grpc_js:${PROTO_DIR} \
--plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin \
-I ./proto \
proto/*.proto

# Generate TypeScript code (d.ts)
npx grpc_tools_node_protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--ts_out=grpc_js:${PROTO_DIR} \
-I ./proto \
proto/*.proto

To generate the protobuf messages and client service stub class from your .proto definitions for Web application, we need:

  • the protoc binary, and
  • the protoc-gen-grpc-web plugin.
  • npm package @grpc/proto-loader — for generate types.

You can download the protoc-gen-grpc-web protoc plugin from release page. If you don’t already have protoc installed, you will have to download it first from here. Make sure they are both executable and are discoverable from your PATH. For example, in MacOS, you can do:

$ sudo mv ~/Downloads/protoc-gen-grpc-web-1.4.1-darwin-x86_64 \ 
/usr/local/bin/protoc-gen-grpc-web
$ sudo chmod +x /usr/local/bin/protoc-gen-grpc-web

When you have both protoc and protoc-gen-grpc-web installed, you can now run this command:

#!/bin/bash

PROTO_DIR=./client/proto

# Generate Types
npx proto-loader-gen-types --longs=String --enums=String \
--defaults --oneofs --grpcLib=@grpc/grpc-js \
--outDir=${PROTO_DIR} proto/*.proto

# Generate JS and TS code
protoc -I=./proto ./proto/*.proto \
--js_out=import_style=commonjs:${PROTO_DIR} \
--grpc-web_out=import_style=typescript,mode=grpcwebtext:${PROTO_DIR}

gRPC-Web have two wire format mode=grpcwebtext (payload is base64) and mode=grpcweb (payload is the binary format).

Implement for Server

First, we need to define UserService and set methods for processing requests:

import {
ServerUnaryCall,
sendUnaryData,
ServerWritableStream,
ServerReadableStream,
ServerErrorResponse,
} from "@grpc/grpc-js";
import { Empty } from "google-protobuf/google/protobuf/empty_pb";
import { IUsersServer } from "./proto/user_grpc_pb";
import { User, UserRequest } from "./proto/user_pb";

export const UsersServer: IUsersServer = {
getUser(call: ServerUnaryCall<UserRequest, User>, callback: sendUnaryData<User>) {
console.log(`getUser: return users by id`);
...
},

getUsers(call: ServerWritableStream<Empty, Empty>) {
console.log(`getUsers: streaming all users.`);
...
},

createUsers(call: ServerReadableStream<Empty, User>, callback: sendUnaryData<Empty>) {
console.log(`createUsers: creating new users from stream.`);
...
},

createUser(call: ServerUnaryCall<User, Empty>, callback: sendUnaryData<Empty>) {
console.log(`createUser: creating new user from unary data.`);
...
},
};

Second, we implement user service in server.

import { Server, ServerCredentials } from "@grpc/grpc-js";
import { UsersService } from "./proto/user_grpc_pb";
import { UsersServer} from "./services";

const server = new Server();
const port = 9090;
const uri = `0.0.0.0:${port}`;

console.log(`Listening on ${uri}`);

server.addService(UsersService, UsersServer);
server.bindAsync(uri, ServerCredentials.createInsecure(), (err) => {
if (err) console.log(err);
server.start();
});

With the server created, the .addService method is called to add our defined services to the server. When done, you start your server with server.start(). You always have to start your server immediately after adding the service else it will throw an error.

Implement for Web

For the web client, we need a library gRPC-Web, which provides a JavaScript library that lets browser clients access a gRPC service. Currently, gRPC-Web is now Generally Available, and considered stable enough for production use. gRPC-Web communicates with gRPC service via a proxy gateway (default is Envoy). The general scheme will look something like this:

Next up, we need to configure the Envoy proxy to forward the browser’s gRPC-Web requests to the backend. Put this in an envoy.yaml file. Here we configure Envoy to listen at port :8080, and forward any gRPC-Web requests to a cluster at port :9090. The best way to do this is in docker. Attention, client streaming does not work on the web (https://github.com/grpc/grpc-web/issues/1205).

For example, getting a list of users would look like this. You need to connect to the server and use the generated code:

import { Empty } from "google-protobuf/google/protobuf/empty_pb";
import { UsersClient } from "./proto/UserServiceClientPb";
import { User } from "./proto/user_pb";

const usersClient = new UsersClient("http://" + "0.0.0.0" + ":8080");

function getUsers() {
return new Promise<User[]>((resolve, reject) => {
const stream = usersClient.getUsers(new Empty());
const users: User[] = [];
stream.on("data", (user) => users.push(user));
stream.on("error", reject);
stream.on("end", () => resolve(users));
});
}

getUsers().then((users) => {
console.log(users.toString());
});

Your application is ready! 🎉🎉🎉 You can see the full code of the project in this repository:

--

--

Mikhail Gus'kov
Frontend Weekly

I have deep expertise in web technologies based on JavaScript.