Skip to content

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:

typescript
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 ​

typescript
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:

typescript
// 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:

typescript
// 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 for RequestConfig.extras. Unconstrained by default; pass a concrete type (like AxiosRequestConfig) for full type checking of framework-specific options.

Request-level config ​

Every method accepts an optional RequestConfig for per-request behavior:

typescript
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 cancellation
  • headers - custom request headers
  • responseType - override response handling (json, blob, text, arraybuffer)
  • extras - framework-specific config, parsed by your adapter
  • metadata - declarative request metadata for routing decisions in your HttpClient implementation