Building a gRPC Server in NodeJS

Adonis Gaitatzis
7 min readOct 24, 2021

--

In this article we will learn how to build a simple gRPC server in NodeJS.

What is gRPC

gRPC is a way for clients and servers to communicate with each other over the Internet. It is intended as an upgrade from REST APIs, with some great features:

  • It transmits faster by using a binary format instead of JSON.
  • It uses a language-agnostic data format, which can be defined and passed. around to clients without the need for per-language developer documentation.
  • It translates its common data format into the native data formats of the server and client.
  • It supports versioning, so future versions of server code don’t require a total rewrite of the client.
  • It defines how to find the endpoints, so URL naming is no longer an issue.
  • It works over HTTP/3, which enables bidirectional streaming with less network latency.

The down sides (at the time of writing) are:

  • It requires custom compiler code for each language to perform all the conversions and binary format conversions.
  • The mechanics of operation are totally obscure to developers. It is not human-readable the way JSON and REST are.
  • The toolchains are new and not very well supported. Sometimes getting a simple call and response to work can take hours or days to get working.
  • It isn’t yet well supported or well documented on all platforms.
  • HTTP/3 is poorly supported by browsers and app software, especially over at Apple.

In this tutorial, I will attempt to walk you through the process I went through to get a simple gRPC client working in React Native on iOS.

How gRPC Works

Basically how gRPC works is that a developer creates a Protobuffer definition (.proto) file, which describes objects and methods which will be provided by a service. This file acts as a sort of developer documentation and a reference for a protobuffer compiler which converts the .proto definitions into the local language’s data types. This is similar to C++ header files or structs, where the the functions and data structures of a class are defined separately from their implementation.

The protobuffer compiler creates a library that the developer can interact with, abstracting the messaging layer of the API.

This allows a developer to write API logic without having to manage API versions, HTTP endpoints, or JSON parsing.

Terminology

gRPC uses slightly different terminology than most developers are familiar with.

In gRPC:

  • A Message is a data structure, which contains primitive properties (known as fields) such as int, bool, or string. Developers can think of a Message as a class or a struct. A list of variable types and their corresponding language implementations can be found in Protocol Buffers Language Guide: Scalar Value Types
    The convention is to name these in camel case, lowercase first letter, for example bigNumber
  • A Remote Procedure Call or rpc is a method or function that can be called remotely. rpcs can take or return zero or more Message parameters. Developers can think of this as a method, function.
    The convention is to name these in camel case, uppercase first letter, for example RpcName
  • A Service is a collection of rpcs. The service acts as a named container for rps, as well as a namespace. So different rpcs can have the same name in different Services, but not the same name name in the same Service. Developers can think of this as a class.
    The convention is to name these in camel case, uppercase first letter, for example ServiceName

Project Overview

The server will be called AuthService and it will contain two routes (routes are like endpoints if you are a web developer):

  1. Login()
  2. Logout()

The Protobuffer File

Here we create a simple Protobuffer definition called authService.protothat provides two methods, Login() and Logout().

For now we will put this in ~/sandbox/protos/authService.proto:

syntax = "proto3";
package authService;
option objc_class_prefix = "RTG"; // important for iOS / Xcode
// Define a service
// Define data types here
message AccountCredentials {
string username = 1;
string password = 2;
}
message OauthCredentials {
string token = 3;
uint32 timeoutSeconds = 4;
}
// Define the service containing methods here
service AuthServiceRoutes {
// Basic function call, makes request and returns value
rpc Login(AccountCredentials) returns (OauthCredentials) {}
rpc Logout(OauthCredentials) returns (OauthCredentials) {}
}

Notice that this file creates a service called AuthServiceRoutes:

service AuthServiceRoutes {
// ...
}

You’ll see that the Login() rpc takes an AccountCredentials Message and returns an OauthCredentials .

rpc Login(AccountCredentials) returns (OauthCredentials) {}

The actual implementation is not defined. Only the function name and parameters. The implementation will be done in the native language of the program, in this case NodeJS.

Likewise the Logout rpc takes an OauthCredentials message and returns one also.

rpc Logout(OauthCredentials) returns (OauthCredentials) {}

The AccountCredentials message is defined as a structure with two properties, username and password . Both are strings. Each property is assigned an ID, in this case 1 and 2.

message AccountCredentials {
string username = 1;
string password = 2;
}

Finally we have this line:

option objc_class_prefix = "AGS"; // important for iOS / Xcode

It’s a best practice in Swift to prefix class names with an acronym that represents the name of a class. in this case we will call the prefix AGS (short for “Authentication gRPC Service.”

This authService.proto file will be available to both the client and server at the time of development, so that the protobuffer compiler can create RPC endpoints.

Setup

Now that we have a .proto file, we can start our project. We will need to create a new project:

$ mkdir -p ~/sandbox/nodejs-grpc-auth-service
$ cd ~/sandbox/nodejs-grpc-auth-service
$ npm init

Follow the steps in the guided setup of the project, then install dependencies. We will be using the official grpc-js module, which runs the gRPC service over HTTP/3.

We will also use the official proto-loader module, which compiles the .proto file into your project at runtime.

$ npm install --save @grpc/grpc-js
$ npm install --save @grpc/proto-loader

Implementing

To get this running, we need to:

  1. Load the Protobuffer file
  2. Implement the RPC calls
  3. Provide the gRPC service

Load the Protobuffer file

Let’s presume you put the .proto file into the ../protos folder, relative to the server path. This is common if you have the server and multiple clients sharing the same .proto file during development.

Create a server.js and enter the following to load the required libraries and load the ProtoBuffer file:

const PROTO_PATH = __dirname + '/../protos/authService.proto';
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')

Now we can load the .proto file and tell NodeJS how to interpret it:

// suggested options for similarity to loading grpc.load behavior
const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String, // JavaScript doesn't support long ints
enums: String, // JavaScript doesn't support enum types
defaults: true,
oneofs: true
}
)
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition)
// grab the authService package from the protobuf file
const authService = protoDescriptor.authService

Implement the business logic

We will create an AuthServer class, which corresponds to the name of the .proto file. Since we will be tracking logins and keeping track of authentication tokens, let’s create an .authTokens and .activeUsers hash maps to track our logged-in users.

class AuthServer {
constructor () {
this.authTokens = {}
this.activeUsers = {}
}
}

for .login() , let’s take a username and password. We can verify the username against currently the .activeUsers to decide if we should do a login or an auth token refresh. Then we can create an object compatible with the Protobuffer file’s OauthCredentials to return. We will add the token into the .authTokens hash so that the .logout() function can look up the user by authentication token.

class AuthServer {
// constructor() {}
login (username, password) {
let loginData = {}
if (username in this.activeUsers) {
loginData = this.activeUsers[username]
this.authTokens[loginData.authToken].refreshExpiration()
} else {
loginData = new AuthLogin(username, password)
this.authTokens[loginData.authToken] = loginData
this.activeUsers[username] = loginData.authToken
}
const currentTime = new Date()
const oauthCredentials = {
token: loginData.authToken,
timeoutSeconds: Math.floor((loginData.expires - currentTime) / 1000)
}
return oauthCredentials
}

To support this, let’s create a AuthLogin object which can do neat things like generating magic numbers for the token and refresh the authorization expiration time:

class AuthLogin {
constructor (username, password) {
this.username = username
this.authToken = Math.random().toString(16).substr(2, 8)
this.refreshExpiration()
}
refreshExpiration () {
const currentTime = new Date()
this.expires = currentTime.setDate(currentTime.getDate() + 1)
}
}

The .logout() method is simpler: it attempts to look up the token in the .activeTokens and then removes the resulting user information from both .activeTokens and .activeUsers if it’s found. It returns blank data for the OauthCredentials Message.

class AuthServer {
// constructor() {}
// login(username, password) {}
logout () {
if (authToken in this.authTokens) {
const loginData = this.authTokens[authToken]
delete this.activeUsers[loginData.username]
delete this.authTokens[authToken]
}
return { token: '', timeoutSeconds: 0 }
}
}

Then we can instantiate this Authentication server :

const authServer = new AuthServer()

Binding the logic to gRPC calls

We must implement two rpc calls: Login() and Logout(). The convention in NodeJS is to name methods using a lowercase first letter, which is amicable to the protocol buffer compiler. So our NodeJS methods will be named .login() and .logout()

Each gRPC receives two parameters:

  • call , which contains the function parameters and other information about the client, etc, and
  • callback, which you will use to respond to the client.

This is similar to ExpressJS’s req and res parameters on REST API endpoints.

The login() rpc will simply call the authServer.login() method:

const login = (call, callback) => {
let username = call.request.username
let password = call.request.password
console.log(`Login for username: '${username}'`)
const authData = authServer.login(username, password)
callback(null, authData)
}

and likewise the logout() rpc will call the authServer.logout() method:

const logout = (call, callback) => {
let authToken = call.request.token
console.log(`Logout for token: '${authToken}'`)
const authData = authServer.logout(authToken)
callback(null, authData)
}

Provide the gRPC service

Now that we have defined these methods, we can bind them to a gRPC service:

function getGrpcServer () {
const grpcServer = new grpc.Server()
grpcServer.addService(authService.AuthServiceRoutes.service, {
login: login,
logout: logout
})
return grpcServer
}

And finally host the gRPC service on port 50051 . Since we are testing we will use an insecure channel ( ServerCredentials.createInsecure() )

const grpcServer = getGrpcServer()
console.log('Starting gRPC server on port 0.0.0.0:50051...')
grpcServer.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(), () => {
grpcServer.start()
}
)

If you wanted to host an SSL-encrypted gRPC service, you can use the ServerCredentials.createSsl() method by providing root certs, keyCertPairs, and a boolean option to check client certificates.

Run

Now that you’ve got the code, you can run a gRPC service on localhost. This service will reset all login data when restarted since we aren’t yet using persistent storage, but it is enough to get started.

$ node server.js
Starting gRPC server on port 0.0.0.0:50051...

Need a client? Try this tutorial to build a gRPC Client in iOS / iOS Swift.

--

--

Adonis Gaitatzis
Adonis Gaitatzis

Written by Adonis Gaitatzis

Is a technology nerd who can synthesize any business need into a technology solution. Fun projects have included brain imaging tech, ML, and blockchain.

No responses yet