@@ -777,6 +777,59 @@ client/server sees protobuf-es's types and conventions:
777777 optional fields.
778778- **int64 as `bigint`.** 64-bit integer fields (`int64`, `uint64`, `sint64`,
779779 ` fixed64` , `sfixed64`) are represented as `bigint`, following protobuf-es.
780+ - **Standalone functions, not a client class.** Each RPC is emitted as a
781+ top-level `export async function` that takes its config per call as a required
782+ options argument (`baseURL` required; `fetch`, `headers`, `signal` optional).
783+ The default plain-interface mode still emits a `class` you `new` and configure
784+ once.
785+
786+ ` ` ` ts
787+ import { getUser } from "./gen/user_service_client.js";
788+
789+ const opts = { baseURL: "https://api.example.com" };
790+ const user = await getUser({ id: "123" }, opts);
791+ ` ` `
792+
793+ Because every RPC is an independent top-level function, bundlers **tree-shake**
794+ the ones you don't import — using a single method doesn't pull the whole
795+ service into your bundle. There is no client object to construct or pass
796+ around; hold the options object (plain data) if you want to reuse config.
797+
798+ The options type is a **shared `RequestOptions`** (emitted once in the shared
799+ ` client.ts` ), so it is not duplicated per service. A service that declares
800+ typed headers instead gets a `{Service}RequestOptions` that **`extends
801+ RequestOptions`** and adds only its typed header properties :
802+
803+ ` ` ` ts
804+ // client.ts (shared)
805+ export interface RequestOptions { baseURL: string; fetch?: typeof fetch; headers?: Record<string, string>; signal?: AbortSignal; }
806+ // token_service_client.ts (declares X-API-Key + X-Request-ID headers)
807+ export interface TokenServiceRequestOptions extends RequestOptions { apiKey?: string; requestId?: string; }
808+ ` ` `
809+
810+ Note that a typed header is its **own top-level property** (`apiKey`), not an
811+ entry in `headers` — the generated function maps it to the real header name
812+ (`X-API-Key`) for you. `headers` stays available as an escape hatch for headers
813+ the proto doesn't declare.
814+
815+ Requiring `baseURL` on every call is the cost of statelessness, but it does
816+ not have to be repeated : declare the options object once, typed as the
817+ service's own options type so its typed header properties are checked, and
818+ reuse the reference. It is plain data, so spreading it per call layers on
819+ per-call extras (`signal`, a typed header property) without mutating the shared
820+ config. Spreading replaces properties rather than deep-merging them, so an
821+ ` opts` that sets `headers` needs that record merged explicitly
822+ (`headers : { ...opts.headers, "X-Trace-Id": id }`); typed header properties are
823+ top-level and so compose cleanly :
824+
825+ ` ` ` ts
826+ import { issue, type TokenServiceRequestOptions } from "./gen/token_service_client.js";
827+
828+ const opts: TokenServiceRequestOptions = { baseURL: "https://api.example.com", apiKey };
829+
830+ await issue({ subject: "user-1" }, opts);
831+ await issue({ subject: "user-2" }, { ...opts, requestId: crypto.randomUUID() });
832+ ` ` `
780833
781834# ## Server-streaming (SSE)
782835
@@ -840,7 +893,7 @@ it in the registry. Proto errors are protobuf-es messages, so they carry a
840893`$typeName` discriminant :
841894
842895` ` ` ts
843- const r = await client. getAccount({ id });
896+ const r = await getAccount({ id }, { baseURL: "https://api.example.com" });
844897if (!r.ok) {
845898 const e = r.error;
846899 if (e instanceof ValidationError) {
0 commit comments