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 genericNumber
BigInt
cannot be serialized includingJSON.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.
Chuma Takahiro