How do you to implement a GRPC server in TypeScript?
Asked Answered
P

3

8

I am trying to use @grpc/proto-loader to do dynamic code generation of the protobuf files to implement a simple server but in Typescript.

I've gotten as far as

import { Server, loadPackageDefinition, ServerCredentials } from "grpc";
import { loadSync } from "@grpc/proto-loader";

const packageDefinition = loadSync(__dirname + "/protos/ArtifactUpload.proto");
const protoDescriptor = loadPackageDefinition(packageDefinition);
const impl = {
};
const server = new Server();
server.addService(protoDescriptor.ArtifactUpload, impl);
server.bind('0.0.0.0:50051', ServerCredentials.createInsecure());
server.start();

So I have two problems

  1. in the Javascript examples they use protoDescriptor.XXX.service however, there's no service property in protoDescriptor.ArtifactUpload
  2. if I try to add implementation methods in impl, the compiler also fails to compile.

Since the Javascript example works, I am thinking that questions along the line of add new property in Typescript object may be able to add the necessary service type. However, I haven't had luck so far.

My Protobuf is

syntax = "proto3";

service ArtifactUpload {
   rpc SignedUrlPutObject (UploadRequest) returns (SignedUrlPutObjectResponse) {}
}

message UploadRequest {
  string message = 1;
}

message SignedUrlPutObjectResponse {
  string reply = 1;
}
Preussen answered 10/11, 2020 at 21:27 Comment(1)
I have created an example gRPC in node.js typescript, I think you might get some help from it. github.com/mirajehossain/nodejs-grpc-example-typescriptDodie
P
2

I got it working in the end as follows:

In package.json I had the following:

{
  ...
  "scripts": {
    "start": "node index.js",
    "build": "pbjs -t static-module -w commonjs -o protos.js protos/*.proto && pbts -o protos.d.ts protos.js && tsc",
  },
  "dependencies": {
    "@grpc/proto-loader": "^0.5.5",
    "google-protobuf": "^3.13.0",
    "grpc": "^1.24.4",
    "typescript": "^4.0.5"
  },
  "devDependencies": {
    "@types/node": "^14.14.7",
    "protobufjs": "^6.10.1"
  }
}

import { Server, loadPackageDefinition, ServerCredentials, GrpcObject, ServiceDefinition, handleUnaryCall } from "grpc";
import { ISignedUrlPutObjectResponse, IUploadRequest, SignedUrlPutObjectResponse } from "./protos";
import { loadSync } from "@grpc/proto-loader";

const packageDefinition = loadSync(__dirname + "/protos/ArtifactUpload.proto");

interface IArtifactUpload {
  signedUrlPutObject: handleUnaryCall<IUploadRequest, ISignedUrlPutObjectResponse>;
}
interface ServerDefinition extends GrpcObject {
  service: any
}
interface ServerPackage extends GrpcObject {
  [name: string]: ServerDefinition
}
const protoDescriptor = loadPackageDefinition(packageDefinition) as ServerPackage;
const server = new Server();
server.addService<IArtifactUpload>(protoDescriptor.ArtifactUpload.service, {
  signedUrlPutObject(call, callback) {
    console.log(call.request.message);
    console.log(callback);
    callback(null, SignedUrlPutObjectResponse.create({ reply: "hello " + call.request.message }));

  }

});
server.bind('0.0.0.0:50051', ServerCredentials.createInsecure());
server.start();

I use protobufjs to build some of the typings though they are mostly unused as it is not fully compatible with GRPC. However, it does save time with the request and response typings.

I still needed to create the server typings and apply it to the protoDescriptor. Repeating it here for emphasis.

interface IArtifactUpload {
  signedUrlPutObject(call: ServerUnaryCall<IUploadRequest>, callback: ArtifactUpload.SignedUrlPutObjectCallback): void;
}


interface ServerDefinition extends GrpcObject {
  service: any;
}
interface ServerPackage extends GrpcObject {
  [name: string]: ServerDefinition
}

I used any for the service as it was the only one that allowed me to avoid putting in anything specific to IArtifactUpload Ideally the typing for GrpcObject which at present is

  export interface GrpcObject {
    [name: string]: GrpcObject | typeof Client | ProtobufMessage;
  }

should try to provide an object that represents the server.

I linked my solution to https://github.com/protobufjs/protobuf.js/issues/1017#issuecomment-725064230 in case there's a better way that I am missing.

Preussen answered 11/11, 2020 at 0:35 Comment(0)
G
26

[Updated on 14 May 2021]: TypeScript generation via @grpc/proto-loader is now released with version 0.6.0! I've updated my example here to reflect this. You can now install the latest version of proto loader with npm i @grpc/proto-loader which will contain the TS generation script. The instructions below are still valid.


You can use the proto-loader to generate types.

First, install the proto-loader:

npm i @grpc/proto-loader

You can then generate the types like so:

./node_modules/.bin/proto-loader-gen-types --longs=String --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --outDir=proto/ proto/*.proto

Here's the proto file I use for this example:

syntax = "proto3";

package example_package;

message ServerMessage {
  string server_message = 1;
}

message ClientMessage {
  string client_message = 1;
}

service Example {
  rpc unaryCall(ClientMessage) returns (ServerMessage) {}
  rpc serverStreamingCall(ClientMessage) returns (stream ServerMessage) {}
  rpc clientStreamingCall(stream ClientMessage) returns (ServerMessage) {}
  rpc bidirectionalStreamingCall(stream ClientMessage) returns (stream ServerMessage) {}
}

Once the types are generated, you can consume them like so:

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { ProtoGrpcType } from './proto/example';
import { ClientMessage } from './proto/example_package/ClientMessage';
import { ExampleHandlers } from './proto/example_package/Example';
import { ServerMessage } from './proto/example_package/ServerMessage';

const host = '0.0.0.0:9090';

const exampleServer: ExampleHandlers = {
  unaryCall(
    call: grpc.ServerUnaryCall<ClientMessage, ServerMessage>,
    callback: grpc.sendUnaryData<ServerMessage>
  ) {
    if (call.request) {
      console.log(`(server) Got client message: ${call.request.clientMessage}`);
    }
    callback(null, {
      serverMessage: 'Message from server',
    });
  },

  serverStreamingCall(
    call: grpc.ServerWritableStream<ClientMessage, ServerMessage>
  ) {
    call.write({
      serverMessage: 'Message from server',
    });
  },

  clientStreamingCall(
    call: grpc.ServerReadableStream<ClientMessage, ServerMessage>
  ) {
    call.on('data', (clientMessage: ClientMessage) => {
      console.log(
        `(server) Got client message: ${clientMessage.clientMessage}`
      );
    });
  },

  bidirectionalStreamingCall(
    call: grpc.ServerDuplexStream<ClientMessage, ServerMessage>
  ) {
    call.write({
      serverMessage: 'Message from server',
    });
    call.on('data', (clientMessage: ClientMessage) => {
      console.log(
        `(server) Got client message: ${clientMessage.clientMessage}`
      );
    });
  },
};

function getServer(): grpc.Server {
  const packageDefinition = protoLoader.loadSync('./proto/example.proto');
  const proto = (grpc.loadPackageDefinition(
    packageDefinition
  ) as unknown) as ProtoGrpcType;
  const server = new grpc.Server();
  server.addService(proto.example_package.Example.service, exampleServer);
  return server;
}

if (require.main === module) {
  const server = getServer();
  server.bindAsync(
    host,
    grpc.ServerCredentials.createInsecure(),
    (err: Error | null, port: number) => {
      if (err) {
        console.error(`Server error: ${err.message}`);
      } else {
        console.log(`Server bound on port: ${port}`);
        server.start();
      }
    }
  );
}

I've created various examples of how to use gRPC with TypeScript here: https://github.com/badsyntax/grpc-js-typescript

Goering answered 16/12, 2020 at 18:15 Comment(2)
Can't thank you enough for the example repos, and for not using @grpc-tools (it doesn't work on M1 macs yet).Sadoff
I love you man... Which is to say, thanks for those examples!Justajustemilieu
P
2

I got it working in the end as follows:

In package.json I had the following:

{
  ...
  "scripts": {
    "start": "node index.js",
    "build": "pbjs -t static-module -w commonjs -o protos.js protos/*.proto && pbts -o protos.d.ts protos.js && tsc",
  },
  "dependencies": {
    "@grpc/proto-loader": "^0.5.5",
    "google-protobuf": "^3.13.0",
    "grpc": "^1.24.4",
    "typescript": "^4.0.5"
  },
  "devDependencies": {
    "@types/node": "^14.14.7",
    "protobufjs": "^6.10.1"
  }
}

import { Server, loadPackageDefinition, ServerCredentials, GrpcObject, ServiceDefinition, handleUnaryCall } from "grpc";
import { ISignedUrlPutObjectResponse, IUploadRequest, SignedUrlPutObjectResponse } from "./protos";
import { loadSync } from "@grpc/proto-loader";

const packageDefinition = loadSync(__dirname + "/protos/ArtifactUpload.proto");

interface IArtifactUpload {
  signedUrlPutObject: handleUnaryCall<IUploadRequest, ISignedUrlPutObjectResponse>;
}
interface ServerDefinition extends GrpcObject {
  service: any
}
interface ServerPackage extends GrpcObject {
  [name: string]: ServerDefinition
}
const protoDescriptor = loadPackageDefinition(packageDefinition) as ServerPackage;
const server = new Server();
server.addService<IArtifactUpload>(protoDescriptor.ArtifactUpload.service, {
  signedUrlPutObject(call, callback) {
    console.log(call.request.message);
    console.log(callback);
    callback(null, SignedUrlPutObjectResponse.create({ reply: "hello " + call.request.message }));

  }

});
server.bind('0.0.0.0:50051', ServerCredentials.createInsecure());
server.start();

I use protobufjs to build some of the typings though they are mostly unused as it is not fully compatible with GRPC. However, it does save time with the request and response typings.

I still needed to create the server typings and apply it to the protoDescriptor. Repeating it here for emphasis.

interface IArtifactUpload {
  signedUrlPutObject(call: ServerUnaryCall<IUploadRequest>, callback: ArtifactUpload.SignedUrlPutObjectCallback): void;
}


interface ServerDefinition extends GrpcObject {
  service: any;
}
interface ServerPackage extends GrpcObject {
  [name: string]: ServerDefinition
}

I used any for the service as it was the only one that allowed me to avoid putting in anything specific to IArtifactUpload Ideally the typing for GrpcObject which at present is

  export interface GrpcObject {
    [name: string]: GrpcObject | typeof Client | ProtobufMessage;
  }

should try to provide an object that represents the server.

I linked my solution to https://github.com/protobufjs/protobuf.js/issues/1017#issuecomment-725064230 in case there's a better way that I am missing.

Preussen answered 11/11, 2020 at 0:35 Comment(0)
O
-1

Never use @grpc/proto-loader, it uses eval under the hood. it is unsecure

Odericus answered 18/6, 2023 at 14:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.