While working at Indeed, I did a fair amount with gRPC. I became rather familiar with the Java, Go, NodeJS, and python implementations. During my vacation between jobs, I decided to revisit one of my old projects and try to migrate it to using gRPC. By doing so, I would be able to support a larger variety of request types (streaming, non-streaming, etc). When I started to look for good NodeJS code samples or reference implementations, I was rather disappointed with what I found. Many of the ones I could get my hands on only demonstrated unary methods and not any of the streaming API’s. After a lot of time digging through source and a few implementations online, I finally assembled a good reference.

In this post, I only wanted to detail what the method calls look like on both ends of the wire. There are some additional best practices that should be taken into consideration, but I do not plan on covering those here.

Protocol Buffers Definition

message Request {
}

message Response {
}

service MyService {
    rpc unaryMethod(Request) returns (Response);
    rpc clientStreamingMethod(stream Request) returns (Response);
    rpc serverStreamingMethod(Request) returns (stream Response);
    rpc bidirectionalStreamingMethod(stream Request) returns (stream Response);
}

Service Implementation

const grpc = require('grpc');
const MyServiceGrpc = grpc.load(require.resolve('./MyService.proto'));

class MyServiceImpl {
    unaryMethod(call, callback) {
        const request = call.request;
        // handle request
        callback('error', response);
    }
    
    clientStreamingMethod(call, callback) {
        call.on('data', (request) => {
            // handle request
        });
        
        call.on('end', () => {
            callback(response);
        });
    }
    
    serverStreamingMethod(call) {
        const request = call.request;
        
        // call N times
        call.write(response);
        
        // call once
        call.end();
    }
    
    bidirectionalStreamingMethod(call) {
        call.on('data', (request) => {
            // handle request
            call.write(response);
        });
        
        call.on('end', () => {
            call.end();
        });
    }
}

const server = new grpc.Server();
server.addService(MyServiceGrpc.MyService.service, new MyServiceImpl());
server.bind('0.0.0.0:1234', grpc.ServerCredentials.createInsecure());
server.start();

Client Usage

const grpc = require('grpc');
const MyServiceGrpc = grpc.load(require.resolve('./MyService.proto'));

const credentials = grpc.credentials.createInsecure();
const client = new MyServiceGrpc.MyService('0.0.0.0:1234', credentials);
const md = new grpc.Metadata();

// unary
client.unaryMethod(request, md, (err, response) => {
    if (err) {
        // handle err
    } else {
        // handle response
    }
});

// client streaming
{
    const call = client.clientStreamingMethod(md, (err, response) => {
        if (err) {
            // handle err
        } else {
            // handle response
        }
    });
    
    // call N times
    call.write(request);
    
    // call once
    call.end();
}

// server streaming
{
    const call = client.serverStreamingMethod(request, md);
    
    call.on('data', (response) => {
        // handle response
    });
    
    call.on('end', () => {
        // handle end of server
    });
}

// bidirectional streaming
{
    const call = client.bidirectionalStreamingMethod(md);
    
    call.on('data', (response) => {
        // handle response
    });
    
    call.on('end', () => {
        // handle end of server
    });
    
    // call N times
    call.write(request);
    
    // call once
    call.end();
}

Other Points of Reference

Code in grpc/grpc-node

Source code is always a great point of reference. gRPC has several good integration tests where you can see some of these approaches being used.