Trying out connect-web
An article about connect-web was trending on Hatena Bookmark. I was curious, so I tried it out.
The sample code is placed in the following repository.
Preface: A rough understanding of gRPC and connect-web
gRPC is a protocol for implementing RPC (Remote Procedure Call). This protocol cannot be used(?) from the browser side, so you will need to use something called gRPC-Web, which is a browser-oriented gRPC. In that case, it seems necessary to set up a proxy between the browser and the server. (Probably)
Therefore, a set of libraries called Connect was developed to build an HTTP API compatible with gRPC. Thanks to this, it is possible to use gRPC from the browser side without the need to set up a proxy.
The above page has items that the backend is connect-go and the frontend is connect-web. connect-web is a small library for running RPC from the browser. It's a type-safe library, so type completion works. connect-go allows you to create a Connect service in go.
Therefore, the development of the frontend will use connect-web. From here on, I will introduce the work of the frontend. By the way, I will use React.
What I tried
The frontend side mainly consists of the following two tasks.
- Generate TypeScript files from Protocol Buffer schema
- Implement gRPC client from generated TypeScript files
1. Generate TypeScript files from Protocol Buffer schema
You need a schema, a ProtocolBuffer schema, to communicate with gRPC. I will use what is already available.
Specifically, it is a schema like the following.
syntax = "proto3";
service ElizaService {
rpc Say(SayRequest) returns (SayResponse) {}
}
message SayRequest {
string sentence = 1;
}
message SayResponse {
string sentence = 1;
}
To generate TypeScript code, use the CLI called buf
.
Write the following definition file to use with buf.
# buf.gen.yaml
# buf.gen.yaml defines a local generation template.
# For details, see https://docs.buf.build/configuration/v1/buf-gen-yaml
version: v1
plugins:
- name: es
path: node_modules/.bin/protoc-gen-es
out: gen
# With target=ts, we generate TypeScript files.
# Use target=js+dts to generate JavaScript and TypeScript declaration files
# like remote generation does.
opt: target=ts
- name: connect-web
path: node_modules/.bin/protoc-gen-connect-web
out: gen
# With target=ts, we generate TypeScript files.
opt: target=ts
This is configuration information on what kind of output to produce when you do buf generate
mentioned later.
I think it's something like a yaml file for codegen.
To run this, let's install the following module.
# plugin
yarn add --dev @bufbuild/protoc-gen-connect-web @bufbuild/protoc-gen-es
# runtime
yarn add @bufbuild/connect-web @bufbuild/protobuf
- plugin
- protoc-gen-es
- Generate basic types such as request and response messages
- protoc-gen-connect-web
- Generate services from Protocol Buffer schema
- protoc-gen-es
- runtime
- bufbuild/connect-web
- Provides clients for Connect and gRPC-web protocols
- bufbuild/protobuf
- Provides serialization for basic types
- bufbuild/connect-web
Next, let's install buf
.
I installed it with brew.
brew install bufbuild/buf/buf
# ref: https://github.com/bufbuild/buf#installation
Let's generate TypeScript files from the ProtocolBuffer schema.
buf generate --template buf.gen.yaml buf.build/bufbuild/eliza
Upon success, the following two TypeScript files will be generated:
- gen/buf/connect/demo/eliza/v1/eliza_connectweb.ts
- gen/buf/connect/demo/eliza/v1/eliza_pb.ts
eliza_connectweb.ts
includes the following code:
// eliza_connectweb.ts
import { SayRequest, SayResponse } from "./eliza_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
export const ElizaService = {
typeName: "ElizaService",
methods: {
say: {
name: "Say",
I: SayRequest,
O: SayResponse,
kind: MethodKind.Unary,
},
},
} as const;
eliza_pb.ts
includes the following code:
export class SayRequest extends Message<SayRequest> {
/**
* @generated from field: string sentence = 1;
*/
sentence = "";
constructor(data?: PartialMessage<SayRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime = proto3;
static readonly typeName = "buf.connect.demo.eliza.v1.SayRequest";
# ... 省略 ...
}
/**
* SayResponse describes the sentence responded by the ELIZA program.
*
* @generated from message buf.connect.demo.eliza.v1.SayResponse
*/
export class SayResponse extends Message<SayResponse> {
/**
* @generated from field: string sentence = 1;
*/
sentence = "";
constructor(data?: PartialMessage<SayResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime = proto3;
static readonly typeName = "buf.connect.demo.eliza.v1.SayResponse";
# ... 省略 ...
}
With this, the preparation is complete.
2. Implementing a gRPC client from the generated TypeScript files
Let's implement a gRPC client.
A gRPC client can be generated with createPromiseClient
.
You need to pass a service and transport(?) as arguments when generating.
It's easier to understand by looking at the code, so please look at the following code:
// client.ts
import { useMemo } from "react";
import { ServiceType } from "@bufbuild/protobuf";
import {
createConnectTransport,
createPromiseClient,
PromiseClient,
Transport,
} from "@bufbuild/connect-web";
const transport = createConnectTransport({
baseUrl: "https://demo.connect.build", # バックエンド側のURL
});
export function useClient<T extends ServiceType>(service: T): PromiseClient<T> {
return useMemo(() => createPromiseClient(service, transport), [service]);
}
Let's try using this client.
// App.tsx
import { createConnectTransport, Interceptor } from "@bufbuild/connect-web";
import { ElizaService } from "../gen/buf/connect/demo/eliza/v1/eliza_connectweb";
import { useClient } from "./client";
function App() {
const client = useClient(ElizaService);
client
.say({
sentence: "hello",
})
.then(({ sentence }) => {
console.log(sentence);
});
// ...
}
In this way, ProtocolBuffers' ElizaService becomes available for type completion. It feels good!
In conclusion
I was surprised that it worked surprisingly smoothly.
Share
Related tags
- Stuck on the path in zod's refine
- Committing to Git from iPhone using Obsidian
- Tried Running ERNIE-ViLG on Google Colaboratory
- Useful Prompt Phrase Collection for Midjourney and StableDiffusion
- Trying to Integrate Dagger for CI/CD with GithubActions and CircleCI
- Thoughts on Using Ruby on Rails in Business
- It's become harder to "ask casually" since remote work started
- How to Set Up Your Antenna in Technology (Ride on the Shoulders of Giants!)
- Remote Debugging (Break Point) with IntelliJ + TypeScript + Docker
- Deploying Cloud Run in Just 3 Steps (golang)