Smart Client Library
Generate typed API clients from OpenAPI and Proto specs. Consuming a service feels like calling local functions — transport, retry, auth, and telemetry are handled automatically.
Overview
@putnami/client bridges the gap between API definition and consumption:
Service A (defines API) Service B (consumes API)
├── openapi.json → ├── UsersClient (generated)
├── api.proto → │ ├── .list()
└── clientGenerator() │ ├── .getById({ id })
│ └── .create({ name, email })
└── "feels like local dev"Setup
Add the clientGenerator plugin to the service that exposes the API:
import { application, api, http, openapi, proto, grpc } from '@putnami/application';
import { clientGenerator } from '@putnami/client';
const app = application()
.use(http({ port: 3000 }))
.use(api())
.use(openapi({ title: 'Users API', version: '1.0.0' }))
.use(proto({ packageName: 'users.v1' }))
.use(grpc())
.use(clientGenerator({ packageName: '@myorg/users-client' }));Run bunx putnami build --impacted to generate the client at clients/ts/.
Usage
import { UsersClient } from '@myorg/users-client';
import { ClientBuilder } from '@putnami/client';
const users = await ClientBuilder.for(UsersClient)
.baseUrl('http://users-api:3000')
.checkDrift()
.build();
const user = await users.getUser({ id: '123' });
// user: { id: string, name: string, email: string } — fully typedThe ClientBuilder auto-negotiates transport and encoding. For synchronous initialization use .buildSync() instead.
Architecture
Transport
Determined at generation time from the API spec:
| Spec | Transport | Protocol |
|---|---|---|
| OpenAPI | HTTP/JSON | Standard REST |
| Proto | Connect | Connect protocol over HTTP |
When both specs exist, Connect is preferred (binary efficiency, streaming support).
Interceptor Chain
Every outgoing request passes through:
Auth → Telemetry → Routing Context → Retry → Transport| Interceptor | What it does |
|---|---|
| Auth | Forwards JWT from incoming request, or obtains client credentials token |
| Telemetry | Records client.{service}.request, .error, .duration metrics |
| Routing Context | Propagates trace ID, request ID, region, experiment headers |
| Retry | Exponential backoff with jitter on transient failures (429, 502, 503, 504) |
| Circuit Breaker | Fail-fast when downstream is unhealthy, with optional health probing |
Service Discovery
URLs resolved from configuration:
# conf/.env.local.yaml
client:
services:
users-api: http://localhost:3000
# conf/.env.production.yaml
client:
services:
users-api: https://users-api.internal:443Or environment variables: CLIENT_SERVICE_USERS_API_URL=http://...
DI Integration
import { set, get } from '@putnami/runtime';
set(UsersClient, new UsersClient({ baseUrl, transport: 'http' }));
const users = get(UsersClient);Error Handling
import { ClientRequestError, ClientServerError } from '@putnami/client';
try {
await users.getUser({ id: 'invalid' });
} catch (error) {
if (error instanceof ClientRequestError) { /* 4xx */ }
if (error instanceof ClientServerError) { /* 5xx */ }
}Circuit Breaker
Prevent cascading failures with the circuit breaker interceptor:
import { circuitBreakerInterceptor } from '@putnami/client';
const users = new UsersClient({
baseUrl: 'http://users-api:3000',
transport: 'http',
interceptors: [
circuitBreakerInterceptor({
failureThreshold: 5,
resetTimeoutMs: 30000,
healthCheckUrl: 'http://users-api:3000/_/health',
}),
],
});Three states: Closed (normal) → Open (fail-fast) → Half-open (trial requests). When healthCheckUrl is set, the breaker probes it while open and transitions to half-open when the service recovers.
Proto Binary Encoding
Generated proto clients use binary encoding automatically — zero config. The generated client embeds proto field metadata at generation time. If the service doesn't support Connect, the ClientBuilder falls back to HTTP/JSON.
Spec Drift Detection
Generated clients embed a spec hash. Enable drift detection to warn at startup if the API has changed:
const users = await ClientBuilder.for(UsersClient)
.baseUrl('http://users-api:3000')
.checkDrift()
.build();
// Logs a warning if spec hash differs from live service — non-blockingStreaming
Streaming RPCs use WebSocket transport automatically. Server-streaming, client-streaming, and bidirectional streaming are all supported:
const stream = client.watchOrders({ userId: '123' });
stream.onMessage((order) => console.log('New order:', order));
stream.onComplete(() => console.log('Done'));
stream.cancel(); // clean upCode Generation Pipeline
OpenAPI / Proto spec
↓
Spec Reader → Intermediate Representation (IR)
↓
TS Generator → Generated Client PackageThe IR is language-agnostic. TypeScript is the first target; Go and Python generators can be added by implementing a new emitter from the same IR.
Configuration Reference
ClientBuilder (recommended)
| Method | Description |
|---|---|
.baseUrl(url) |
Service base URL (required) |
.clientId(id) |
Client identity for X-Client-Id |
.timeout(ms) |
Request timeout |
.retry(config) |
Retry configuration |
.interceptors([...]) |
Custom interceptors |
.checkDrift() |
Enable spec drift detection |
.build() |
Async build with transport negotiation |
.buildSync() |
Sync build using embedded metadata |
ClientConfig (manual)
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl |
string |
required | Service base URL |
transport |
'http' | 'connect' |
required | Transport protocol |
timeoutMs |
number |
30000 | Request timeout |
retry.maxRetries |
number |
3 | Max retry attempts |
retry.baseDelayMs |
number |
200 | Base backoff delay |
retry.maxDelayMs |
number |
5000 | Max backoff delay |
retry.retryableStatuses |
number[] |
[429,502,503,504] | Retryable HTTP codes |
retry.jitter |
boolean |
true | Randomize delay |
interceptors |
Interceptor[] |
[] | Custom interceptors |
ClientGeneratorConfig
| Option | Type | Default | Description |
|---|---|---|---|
output |
string |
clients/ts |
Output directory |
packageName |
string |
auto | npm package name |
targets |
('ts')[] |
['ts'] |
Generation targets |