implement gRPC-Web client

When you want to access gRPC services from browsers, you need to implement gRPC-Web clients. Browsers’ fetch lacks several functionalities to implement full spec gRPC for years.

While gRPC and protobuf was initially led by Google, protobuf-javascript is not maintained actively.

At this time, ConnectRPC’s web client and protobuf-es set is more active.

Stub generation

Client stub can be generated with buf generate.
You can install buf CLI and plugins with npm or pnpm.

npm install --save-dev @bufbuild/buf @bufbuild/protoc-gen-es @connectrpc/protoc-connect-gen-es

buf generate sub command refers to buf.gen.yaml config, which should be placed on the top directory including your .proto files:

version: v1
plugins:
# for @bufbuild/protoc-gen-es
- plugin: es
  out: src/
# for @connectrpc/protoc-connect-gen-es
- plugin: connect-es
  out: src/

Client code

ConnectRPC can change protocols between ConnectRPC, gRPC and gRPC-web by switching transports.
createGrpcWebTransport() provides gRPC-web access.
Basic client init will be as follows:

import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { createPromiseClient } from "@connectrpc/connect";

// import Client stub generated by `buf generate`
import { GeneratedService } from './generated_service_connect.js';

const transport = createGrpcWebTransport({
  baseUrl: "https://example.com",
  useBinaryFormat: true,
  credentials: "include",
  interceptors: [],
});

const client = createPromiseClient(GeneratedService, transport);

You can use createPromiseClient() both for Unary and Streaming methods.

Method call

buf generate creates each message type like described in official example.
While you can create each object with new User(), Connect client accepts just plain objects:

const result = await client.createUser({
  firstName: "Homer",
  lastName: "Simpson",
  active: true,
  locations: ["Springfield"],
  manager: {
    firstName: "Montgomery",
    lastName: "Burns",
  },
});

This way looks straight forward in many usages.

  • Prop names need to be camelCase, even proto is written in snake_case.
  • Returning value is also just plain object, having camelCase props.

Interceptors just work

As official doc describes, interceptors can be defined as plain function that receives request object. The request detail is node described in docs, you need to consult with its implementation.
If you want to modify HTTP headers, you can do it through req.header as a Fetch API Headers interface.

An interceptor can handle both unary and stream transport with internal conditional switch. Stream responses need to be handled with generators.
At this time, unary/stream requests have the same behavior, as gRPC-web haven’t supported client-side stream for years.

BigInt conversion

ConnectRPC provides generally better gRPC-web client, but you need to pay extra attentions on int64 values. Connect treats protobuf int64 as javascript BigInt having several limitations:

  • BigInt cannot be mixed with generic Number
  • BigInt cannot be serialized including JSON.stringify()

For example, several data stores like Redux require serialization.
BigInt is not compatible with other types, and Connect doesn’t provide type conversion.
If you put generic Number into RPC request, clients throw just int32 to its service.

You may need to manually cast with Number() and BigInt() in many places.

⁋ Nov 21, 2023↻ Sep 2, 2024
中馬崇尋
Chuma Takahiro