正在加载,请稍候…

gRPC with Node.js and TypeScript: Streaming and Protocol Buffers

Build high-performance gRPC services with Node.js and TypeScript. Learn protocol buffers, service definitions, unary and streaming RPCs, interceptors, and gRPC-web.

gRPC with Node.js and TypeScript

Protocol Buffer Definition

// orders.proto
syntax = "proto3";
package orders.v1;

import "google/protobuf/timestamp.proto";

service OrderService {
  // Unary RPC
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc CreateOrder(CreateOrderRequest) returns (Order);

  // Server streaming: real-time order updates
  rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);

  // Client streaming: bulk import
  rpc ImportOrders(stream CreateOrderRequest) returns (ImportResult);

  // Bidirectional streaming: live chat
  rpc LiveSupport(stream SupportMessage) returns (stream SupportMessage);
}

message Order {
  string id = 1;
  string customer_id = 2;
  OrderStatus status = 3;
  double total = 4;
  google.protobuf.Timestamp created_at = 5;
  repeated OrderItem items = 6;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
}

TypeScript Server Implementation

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';

const packageDef = protoLoader.loadSync('./protos/orders.proto', {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
});
const proto = grpc.loadPackageDefinition(packageDef) as any;

const orderServiceImpl = {
  // Unary
  async getOrder(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
    try {
      const order = await orderRepo.findById(call.request.id);
      if (!order) {
        return callback({ code: grpc.status.NOT_FOUND, message: `Order ${call.request.id} not found` });
      }
      callback(null, orderToProto(order));
    } catch (err) {
      callback({ code: grpc.status.INTERNAL, message: 'Internal error' });
    }
  },

  // Server streaming
  watchOrder(call: grpc.ServerWritableStream<any, any>) {
    const { order_id } = call.request;

    const unsubscribe = orderEvents.subscribe(order_id, (event) => {
      call.write({ event_type: event.type, order: orderToProto(event.order) });

      if (event.type === 'DELIVERED') {
        call.end();
        unsubscribe();
      }
    });

    call.on('cancelled', () => unsubscribe());
  },

  // Client streaming
  async importOrders(call: grpc.ServerReadableStream<any, any>, callback: grpc.sendUnaryData<any>) {
    const orders: Order[] = [];

    call.on('data', async (req) => {
      orders.push(createOrderFromProto(req));
    });

    call.on('end', async () => {
      try {
        const imported = await orderRepo.bulkInsert(orders);
        callback(null, { imported_count: imported.length, failed_count: 0 });
      } catch (err) {
        callback({ code: grpc.status.INTERNAL });
      }
    });
  },
};

const server = new grpc.Server();
server.addService(proto.orders.v1.OrderService.service, orderServiceImpl);
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  console.log('gRPC server running on port 50051');
  server.start();
});

TypeScript Client

const client = new proto.orders.v1.OrderService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

// Unary call
const order = await new Promise<Order>((resolve, reject) => {
  client.getOrder({ id: 'order_123' }, (err: any, response: any) => {
    if (err) reject(err);
    else resolve(response);
  });
});

// Server streaming
const stream = client.watchOrder({ order_id: 'order_123' });
stream.on('data', (event: any) => {
  console.log('Order event:', event.event_type, event.order.status);
});
stream.on('end', () => console.log('Stream ended'));
stream.on('error', (err: Error) => console.error('Stream error:', err));

Interceptors (Middleware)

// Logging interceptor
function loggingInterceptor(
  options: grpc.InterceptorOptions,
  nextCall: (options: grpc.InterceptorOptions) => grpc.InterceptingCall
): grpc.InterceptingCall {
  const start = Date.now();
  return new grpc.InterceptingCall(nextCall(options), {
    start(metadata, listener, next) {
      next(metadata, {
        onReceiveStatus(status, next) {
          console.log(`gRPC ${options.method_definition.path}: ${status.code} in ${Date.now()-start}ms`);
          next(status);
        },
      });
    },
  });
}

const client = new proto.orders.v1.OrderService(
  'localhost:50051',
  grpc.credentials.createInsecure(),
  { interceptors: [loggingInterceptor] }
);

vs REST Comparison

gRPC:
  + 2-7x faster than REST+JSON (Protocol Buffers binary encoding)
  + Strongly typed contracts (proto files)
  + Streaming support built-in
  + Code generation for all languages
  - Browser support requires gRPC-Web proxy
  - Less debuggable (binary format)
  - Steeper learning curve

REST:
  + Universal browser support
  + Human-readable (JSON)
  + Easier to debug
  - No streaming
  - No native type safety

gRPC is ideal for internal microservices; use REST for public APIs until you need gRPC's performance.