gRPC allows a one-word implementation to write an API where a client requests for data once but the server returns a stream of data

This is done due to the fact that gRPC runs on HTTP2, so resource multiplexing comes into the picture and helps server keep pushing data while the client listens for it

Resources

I have committed all the protofiles in detailed branches at this github repository for your perusal

crew-guy/gRPC
A repository that explains setting up modern, fast, language agnostic APIs using the gRPC framework - crew-guy/gRPC
GitHub repo link

This is the file structure that I have followed while designing this API.


root_folder/
|-- client/
|   |-- client.js
|-- node_modules  
|-- protos/
|   |-- primenum.proto
|-- server/
     |-- index.js
     |-- protos/
        |-- primenum_pb.js
        |-- primenum_grpc_pb.js

0. Sample Problem

In this exercise, we will be building a Prime Number Decomposition API using server streaming

The function takes a Request message that has one integer, and returns a stream of Responses that represent the prime number decomposition of that number

Pseudo code

k = 2
N = 210
while N > 1:
    if N % k == 0:   // if k evenly divides into N
        print k      // this is a factor
        N = N / k    // divide N by k so that we have the rest of the number left.
    else:
        k = k + 1

1. Writing the protofile

This is pretty basic and will involve

TYPE HTML
gRPC Request INTEGER Simple message object
gRPC Response STREAM OF INTEGERS Simple message object
Service FUNCTION rpc CalcPn(PnRequest) returns (stream PnResponse){}
syntax="proto3";

package primenum;

service PnService {
    rpc CalcPn(PnRequest) returns (stream PnResponse){}
}

message PnRequest{
    int32 pn = 1;
}

message PnResponse{
    int32 result = 1;
}

Here, the stream keyword next to PnResponse is of key importance as it tells gRPC to generate code accordingly such that the function called from the client will return a stream of responses

2. Generate the protofiles

Packages required : protoc, grpc-tools

1. grpc-tools

npm i grpc-tools

We get PnServiceService and PnServiceClient generated from the "grpc-tools" module that will act on our proto to create the client and service (server)

Output file ⇒ PACKAGENAME_grpc_pb.js

2. protoc

Whereas, the protoc will simply generate the Request and Response objects and give us getters and setters on them

Output filename ⇒ PACKAGENAME_pb.js
sudo protoc -I=. ./protos/primenum.proto \                                        
  --js_out=import_style=commonjs,binary:./server \  
  --grpc_out=./server \
  --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin`

Here the inside the main project directory, we have a client, server and protos subdirectory. Currently, we are generating the API from proto file in the protos directory into the protos subdirectory inside the server directory

3. Setup a gRPC server

npm i grpc google-protobuf

Import the necessary dependencies. Here,

  1. primenum_pb ⇒  Contains all the details about the request and response object
  2. primenum_grpc_pb ⇒  Will help the grpc server create an API from the proto generated code.
const grpc = require("grpc")
const pn = require('./protos/primenum_pb')
const pnService = require('./protos/primenum_grpc_pb')
const grpc = require("grpc")
const chalk = require('chalk')


// Define the function (camelcased version of the function described in the protofile) that will do the calculation

const calcPn = (call, callback) =>
{
    let pnReq = call.request.getPn()
    console.log(chalk.greenBright(pnReq))
    let pnTemp = pnReq
    for(let i = 2; i < pnReq; i++)
    {
        while (pnTemp % i === 0)
        {
            pnTemp = pnTemp / i
            const pnRes = new pn.PnResponse()
            pnRes.setResult(i)
            call.write(pnRes)
        }
    }
    call.end()
}

// Add this service request to the server

server.addService(pnService.PnServiceService, { calcPn })

// START THE SERVER
const URL_ENDPOINT = "127.0.0.1:50051"
server.bind(URL_ENDPOINT, grpc.ServerCredentials.createInsecure())
server.start()

console.log(chalk.yellow(`Server running on ${URL_ENDPOINT}`))


Define the function that will handle the request and return a response. This function is what we defined in the proto file and this is also the function that will be callled from the client

4. Setup the Client Side

Import the necessary dependencies.

  1. primenum_grpc_pb ⇒ Contains proto generated code that will help us setup the client and link it to the server functions we have defined
  2. primenum_pb ⇒ Contains code to help us access and work with the request object
const grpc = require('grpc');
const pn = require('../server/protos/primenum_pb')
const pnService = require('../server/protos/primenum_grpc_pb')

Setup a client using the proto generated code

const pnServiceClient = new pnService.PnServiceClient(
    URL_ENDPOINT,
    grpc.credentials.createInsecure()
)

Configure and send the request object

const pnServiceClient = new pnService.PnServiceClient(
    URL_ENDPOINT,
    grpc.credentials.createInsecure()
)

const pnServiceReq = new pn.PnRequest()
pnServiceReq.setPn(20)
const call = pnServiceClient.calcPn(pnServiceReq, () => {})

Handle the response received on the client side

call.on('status', (status) =>
{
    console.log(status)
})

call.on('data', (response) =>
{
    console.log(`Factor : ${response}`)
})

call.on('error', (error) =>
{
    console.log(error)
})

call.on('end', () =>
{
    console.log('Streaming has ended ........')
})

Add service to server and start it

//* Setup a server
const server = new grpc.Server()

 server.addService(pnService.PnServiceService, { calcPn })

//? START THE SERVER
const URL_ENDPOINT = "127.0.0.1:50051"
server.bind(URL_ENDPOINT, grpc.ServerCredentials.createInsecure())
server.start()

console.log(chalk.green(`Server running on ${URL_ENDPOINT}`))

Conclusion

Congratulations ! You are now equipped with the futuristic skills of server-push-client-stream type API designing. Thanks a lot for reading this blog. If you like more content like this, subscribe to my mailing list