Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. TypescriptSeparator
  4. Smart Client

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 typed

The 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:443

Or 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-blocking

Streaming

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 up

Code Generation Pipeline

OpenAPI / Proto spec
        ↓
    Spec Reader → Intermediate Representation (IR)
        ↓
    TS Generator → Generated Client Package

The 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

On this page

  • Smart Client Library
  • Overview
  • Setup
  • Usage
  • Architecture
  • Transport
  • Interceptor Chain
  • Service Discovery
  • DI Integration
  • Error Handling
  • Circuit Breaker
  • Proto Binary Encoding
  • Spec Drift Detection
  • Streaming
  • Code Generation Pipeline
  • Configuration Reference
  • ClientBuilder (recommended)
  • ClientConfig (manual)
  • ClientGeneratorConfig