A server and client in Node.js

gRPC · Essentials · 15 min

Seeing both ends of a call makes every later topic concrete.

In Node.js the official implementation is @grpc/grpc-js (pure JavaScript). There are two ways to get stubs:

  • Dynamic loading with @grpc/proto-loader: parse the .proto at startup and build client classes and service definitions at runtime. Quick to start, no build step.
  • Static code generation with protoc or Buf plus a plugin (e.g. ts-proto) for typed TypeScript code.

With dynamic loading, grpc.loadPackageDefinition() returns an object mirroring your packages, so hello.v1.Greeter is a client constructor and hello.v1.Greeter.service the service definition.

Server side, you addService(definition, handlers). A unary handler receives call (with call.request, call.metadata) and a callback(err, response). Bind to a port with bindAsync() and credentials; createInsecure() is for local development only.

Client side, construct the stub with an address and credentials, then call methods with (request, callback). Clients are cheap to call but expensive to create: create one per target at startup and reuse it, because it holds the HTTP/2 connection.

Proto-loader options matter: keepCase: true keeps snake_case field names (otherwise they become camelCase), defaults: true fills unset fields with defaults, and longs: String avoids precision loss for 64-bit integers.

Example

// hello.proto: service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }
//              message HelloRequest { string name = 1; }  message HelloReply { string message = 1; }
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";

const def = protoLoader.loadSync("hello.proto", { keepCase: true, defaults: true });
const { hello } = grpc.loadPackageDefinition(def);

const server = new grpc.Server();
server.addService(hello.v1.Greeter.service, {
  SayHello: (call, callback) => callback(null, { message: `Hello, ${call.request.name}` }),
});
server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), (err) => {
  if (err) throw err;
  const client = new hello.v1.Greeter("127.0.0.1:50051", grpc.credentials.createInsecure());
  client.SayHello({ name: "Ann" }, (err, reply) => {
    console.log(reply.message); // Hello, Ann
    client.close();
    server.forceShutdown();
  });
});

Remember

  • @grpc/grpc-js is the Node implementation; proto-loader loads .proto at runtime
  • Unary handlers: (call, callback); clients: stub.Method(req, cb)
  • Create one client per target and reuse it
  • Use longs: String to keep int64 precision

Watch out for

  • Creating a new client per request, paying a new connection and TLS handshake each time.

Check yourself

  1. Why should a Node service reuse one gRPC client instance per target?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: grpc.io/docs/languages/node/basics/