Skip to main content

Using clients

We have already been using the function createClient in the tutorial. The function gives us a client that uses ECMAScript promise objects. In combination with the await keyword, this lets you write asynchronous code in a natural and easily readable way:

import { createClient } from "@connectrpc/connect";
import { ElizaService } from "@buf/connectrpc_eliza.bufbuild_es/connectrpc/eliza/v1/eliza_pb";

const client = createClient(ElizaService, transport);

const res = await client.say({
sentence: "I feel happy.",
});
console.log(res.sentence);

For server-streaming RPCs, the corresponding method on the client will return an async iterable stream of response messages that can be used with the for await...of statement:

for await (const res of client.introduce({ name: "Joseph" })) {
console.log(res);
}

Callbacks

If you prefer a callback-based approach, the client returned by the function createCallbackClient should suit you:

import { createCallbackClient } from "@connectrpc/connect";
import { ElizaService } from "@buf/connectrpc_eliza.bufbuild_es/connectrpc/eliza/v1/eliza_pb";

const client = createCallbackClient(ElizaService, transport);

client.say({ sentence: "I feel happy." }, (err, res) => {
if (!err) {
console.log(res.sentence);
}
});

For server-streaming RPCs, the corresponding method on the client takes two callback functions: one that is called every time a response message arrives, and one that is called at the end of the stream.

import {ConnectError} from "@connectrpc/connect";

client.introduce({name: "Joseph"}, (res) => {
console.log(res);
}, (err?: ConnectError) => {
if (err) {
console.error(err);
}
});

The callback client is particularly useful if you want to migrate an existing code base from gRPC-web to Connect clients.

Managing clients and transports

In practice, you will likely want to avoid creating a new transport every time you want to use a client. It really depends on the framework of your choice, but there usually is a simple solution to avoid repetition.

For example, you can easily create a custom hook in React:

// use-client.ts
import { useMemo } from "react";
import { type DescService } from "@bufbuild/protobuf";
import { createConnectTransport } from "@connectrpc/connect-web";
import { createClient, type Client } from "@connectrpc/connect";

// This transport is going to be used throughout the app
const transport = createConnectTransport({
baseUrl: "https://demo.connectrpc.com",
});

/**
* Get a promise client for the given service.
*/
export function useClient<T extends DescService>(service: T): Client<T> {
// We memoize the client, so that we only create one instance per service.
return useMemo(() => createClient(service, transport), [service]);
}

Usage:

await useClient(ElizaService).say({sentence: "I feel happy."});

Roll your own client

If you find that neither client suits your needs perfectly, it might be an option for you to roll your own. For example, you might prefer Rust-style result types over promise rejections, and could write your own constructor function that uses the neverthrow library for method return values.

For a working example, see the custom-client in the examples-es repo, or take a look at the implementation of createClient and createCallbackClient.