Using Generated Code
Implement the HTTP client interface
The generated client layer is framework-agnostic. You implement the HttpClient interface once, plugging in whatever HTTP library your app uses:
import { HttpClient, RequestConfig } from './generated/clients/http-client';
class MyHttpClient implements HttpClient {
async get<T>(url: string, params?: any, config?: RequestConfig): Promise<T> {
const response = await fetch(url, {
method: 'GET',
headers: config?.headers,
signal: config?.signal,
});
return response.json();
}
async post<T>(url: string, data?: any, config?: RequestConfig): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...config?.headers,
},
body: JSON.stringify(data),
signal: config?.signal,
});
return response.json();
}
// Implement put, delete, patch methods...
}Use the generated clients
import { UserServiceClient } from './generated/clients/user-client';
import { MyHttpClient } from './http-client';
const httpClient = new MyHttpClient();
// No prefix: the proto already declares the full path (/api/v1/users/{user_id})
const userService = new UserServiceClient(httpClient);
// Call the API
const user = await userService.getUser({ userId: '123' });
console.log(user);The baseUrl parameter
The client constructor takes an optional second argument baseUrl that is prepended to every proto-defined request path. It defaults to an empty string, meaning the request uses the path exactly as declared in the proto.
You only need it when the frontend request path differs from what the proto declares — typically when a gateway (e.g. Nginx) adds a prefix during forwarding. For example, if the proto declares /user/get but Nginx rewrites all requests to /api/user/get, pass /api:
// Backend proto: rpc GetUser(...) { option (google.api.http) = { get: "/user/get" }; }
// Frontend goes through Nginx which adds /api → /api/user/get
const userService = new UserServiceClient(httpClient, '/api');If the frontend and backend paths are identical, don't pass it.
Response type unwrapping
Generated clients support response type unwrapping for responses wrapped in { code, data, message } structures. Pass the wrapper type and the field name that carries the data:
// Define response wrapper type
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
// Use generic parameters to auto-unwrap
const userService = new UserServiceClient<ApiResponse<any>, 'data'>(httpClient);
// Return type is automatically inferred as User, not ApiResponse<User>
const user = await userService.getUser({ userId: '123' });The three generic parameters:
TRes- Response wrapper type.void(default) means no wrapping, returns the raw response type. Pass a business wrapper type to auto-unwrap.TDataKey- Field name carrying data in the wrapper. Defaults to'data', can be'payload','result','body', etc.TExtras- Type constraint forRequestConfig.extras. Unconstrained by default; pass a concrete type (likeAxiosRequestConfig) for full type checking of framework-specific options.
Request-level config
Every method accepts an optional RequestConfig for per-request behavior:
await userService.getUser(
{ userId: '123' },
{ silent: true, signal: controller.signal, headers: { 'X-Trace-Id': 'abc' } },
);silent- silence errors at the request level (no UI toasts)signal- AbortController signal for cancellationheaders- custom request headersresponseType- override response handling (json,blob,text,arraybuffer)extras- framework-specific config, parsed by your adaptermetadata- declarative request metadata for routing decisions in your HttpClient implementation