Docs Client library
Client API
The Universal Client provides protocol-agnostic RPC communication with type-safe middleware composition.
Quick Start#
import { Client, HttpTransport, createRetryMiddleware, createAuthMiddleware } from "client";
// Create client with HTTP transport
const client = new Client({ transport: new HttpTransport({ baseUrl: "https://api.example.com" }) })
.use(createRetryMiddleware({ maxRetries: 3 }))
.use(createAuthMiddleware({ token: process.env.API_TOKEN }));
// Make RPC call
const user = await client.call<GetUserRequest, User>(
{ service: "users", operation: "get" },
{ id: 123 }
);
// Streaming
for await (const event of client.stream(
{ service: "events", operation: "watch" },
{ topic: "orders" }
)) {
console.log(event);
}
Client Class#
Constructor#
// With transport directly
const client = new Client(transport);
// With options
const client = new Client({
transport: new HttpTransport({ baseUrl: "/api" }),
defaultMetadata: { auth: { apiKey: "xxx" } },
throwOnError: true, // default: true
});
Methods#
use<TProvides, TRequires>(middleware): Client<TContext & TProvides>#
Add middleware to the client. Returns a new client type with accumulated context.
const client = new Client(transport)
.use(createTimeoutMiddleware({ overall: 5000 })) // Client<TimeoutContext>
.use(createRetryMiddleware()) // Client<TimeoutContext & RetryContext>
.use(createCacheMiddleware({ methods: ["users.get"] })); // Client<... & CacheContext>
call<TReq, TRes>(method, payload, metadata?): Promise<TRes>#
Make a single RPC call.
const user = await client.call<{ id: number }, User>(
{ service: "users", operation: "get" },
{ id: 123 },
{ timeout: { overall: 5000 } } // optional metadata
);
stream<TReq, TRes>(method, payload, metadata?): AsyncIterable<TRes>#
Make a streaming RPC call.
for await (const event of client.stream(
{ service: "events", operation: "watch" },
{ topic: "orders" }
)) {
processEvent(event);
}
close(): Promise<void>#
Close the client and cleanup transport resources.
Type-Safe Middleware#
How It Works#
Each middleware declares what context it provides and requires:
type TypedClientMiddleware<
TProvides = {}, // Context this middleware adds
TRequires = {}, // Context this middleware needs
>;
// Retry provides retry context, requires nothing
createRetryMiddleware(): TypedClientMiddleware<RetryContext, {}>
// A hypothetical middleware requiring retry context
createRetryLoggerMiddleware(): TypedClientMiddleware<{}, RetryContext>
Context Types#
interface RetryContext {
retry: { attempt: number; maxAttempts: number };
}
interface CacheContext {
cache: { hit: boolean; key: string };
}
interface TimeoutContext {
timeout: { overall?: number; perAttempt?: number };
}
interface AuthContext {
auth: { token?: string; apiKey?: string; userId?: string };
}
interface TracingContext {
tracing: { traceId: string; spanId: string; parentSpanId?: string };
}
interface CircuitBreakerContext {
circuitBreaker: { state: CircuitState; failures: number; successes: number };
}
interface RateLimitContext {
rateLimit: { tokensAvailable: number; maxTokens: number; queueSize: number };
}
interface BatchingContext {
batching: { batchKey: string; batchSize: number };
}
interface PaginationContext {
pagination: { page?: number; offset?: number; limit: number };
}
Middleware Reference#
Retry Middleware#
Exponential backoff with jitter.
client.use(createRetryMiddleware({
maxRetries: 3, // default: 3
retryDelay: 1000, // base delay in ms, default: 1000
jitter: 0.1, // jitter fraction 0-1, default: 0.1
shouldRetry: (item, attempt) => item.status.retryable, // custom predicate
onBeforeRetry: async (item, attempt) => ({
shouldRetry: true,
delayMs: 2000, // override delay
}),
onAfterRetry: (success, attempt) => console.log(`Attempt ${attempt}: ${success}`),
}));
Cache Middleware#
LRU + TTL caching using the collections framework.
client.use(createCacheMiddleware({
methods: ["users.get", "users.list"], // required: only these calls are cached
capacity: 100, // max entries, default: 100
ttl: 60000, // time-to-live in ms, default: 60000
// The default key holds the method, the payload and the metadata (auth, pagination)
keyGenerator: (method, payload, metadata) => `${method.service}.${method.operation}:${JSON.stringify([payload, metadata.auth])}`,
shouldCache: (item) => item.status.type === "success",
onStats: (stats) => console.log(`Hit rate: ${stats.hitRate}%`),
statsInterval: 60000, // how often to emit stats
}));
Timeout Middleware#
Three variants for different timeout strategies.
// Per-attempt timeout (each retry gets fresh timeout)
client.use(createTimeoutMiddleware({ perAttempt: 1000 }));
// Overall timeout (entire request including retries)
client.use(createOverallTimeoutMiddleware({ overall: 5000 }));
// Combined
client.use(createCombinedTimeoutMiddleware({
overall: 5000,
perAttempt: 1000,
}));
Auth Middleware#
Authentication injection.
// Bearer token
client.use(createAuthMiddleware({ token: "abc123" }));
client.use(createBearerAuthMiddleware("abc123")); // convenience
// API key
client.use(createAuthMiddleware({ apiKey: "xyz789" }));
client.use(createApiKeyAuthMiddleware("xyz789")); // convenience
// Dynamic auth
client.use(createAuthMiddleware(() => ({
token: getTokenFromStore(),
userId: getCurrentUserId(),
})));
Tracing Middleware#
Distributed tracing (W3C Trace Context compatible).
// Auto-generate trace/span IDs
client.use(createTracingMiddleware());
client.use(createSimpleTracingMiddleware()); // uses crypto.randomUUID if available
// Custom ID generation
client.use(createTracingMiddleware({
generateTraceId: () => uuid.v4(),
generateSpanId: () => uuid.v4(),
serviceName: "my-service",
}));
// Continue existing trace
client.use(createTracingMiddleware({
getCurrentTrace: () => ({
traceId: existingTrace.traceId,
spanId: existingTrace.spanId,
}),
}));
Circuit Breaker Middleware#
Prevents cascading failures.
client.use(createCircuitBreakerMiddleware({
failureThreshold: 5, // failures before opening, default: 5
failureWindow: 10000, // window for counting failures, default: 10s
resetTimeout: 30000, // time before trying again, default: 30s
successThreshold: 2, // successes to close, default: 2
isFailure: (error) => true, // custom failure predicate
onStateChange: (oldState, newState) => console.log(`Circuit: ${oldState} → ${newState}`),
}));
States:
- CLOSED: Normal operation, requests pass through
- OPEN: Service unhealthy, requests fail immediately with
CircuitBreakerError - HALF_OPEN: Testing recovery, limited requests allowed
Rate Limit Middleware#
Token bucket throttling.
// Reject strategy (default)
client.use(createRateLimitMiddleware({
maxRequests: 100, // requests per window, default: 100
window: 60000, // window in ms, default: 60s
strategy: "reject", // throw RateLimitError when exceeded
}));
// Queue strategy
client.use(createRateLimitMiddleware({
maxRequests: 10,
window: 1000,
strategy: "queue", // queue requests, process when capacity available
maxQueueSize: 50, // max queue size, default: 100
onRateLimitExceeded: (stats) => console.log("Rate limited!", stats),
}));
// Per-service rate limits
client.use(createPerServiceRateLimiter({
users: { maxRequests: 100, window: 60000 },
orders: { maxRequests: 50, window: 60000 },
default: { maxRequests: 200, window: 60000 },
}));
Batching Middleware#
Request aggregation.
client.use(createBatchingMiddleware({
maxBatchSize: 10, // max requests per batch, default: 10
maxWaitTime: 10, // max wait before sending, default: 10ms
sameServiceOnly: true, // only batch same-service requests, default: true
getBatchKey: (msg) => msg.method.service, // custom batch key
onBatchSent: (size, key) => console.log(`Batch sent: ${size} requests to ${key}`),
}));
// Adaptive batching
client.use(createAdaptiveBatchingMiddleware({
minBatchSize: 2,
maxBatchSize: 50,
targetLatency: 20, // auto-adjust batch size to target latency
}));
Pagination Middleware#
Automatic pagination handling.
client.use(createPaginationMiddleware({
defaultLimit: 50, // default page size
maxLimit: 1000, // prevent abuse
pageKey: "page", // metadata key for page number
limitKey: "limit", // metadata key for page size
useOffset: false, // use offset instead of page
}));
// Auto-paginate all results
import { paginateAll } from "client/client";
for await (const user of paginateAll(client, { service: "users", operation: "list" }, {})) {
console.log(user);
}
Middleware Ordering#
Middleware executes in onion model order:
client
.use(timeoutMiddleware) // 1st: Outer layer (runs first on request)
.use(retryMiddleware) // 2nd: Middle layer
.use(cacheMiddleware); // 3rd: Inner layer (closest to transport)
Request flow: timeout → retry → cache → transport Response flow: transport → cache → retry → timeout
Recommended Order#
- Timeout (overall) - Fail fast if taking too long
- Retry - Retry failed requests
- Timeout (per-attempt) - Timeout individual attempts
- Cache - Check cache before network
- Circuit Breaker - Fail fast for unhealthy services
- Rate Limit - Throttle requests
- Auth - Add credentials
- Tracing - Add trace context
Error Handling#
ClientError#
Thrown when response has error status (when throwOnError: true):
try {
await client.call(method, payload);
} catch (error) {
if (error instanceof ClientError) {
console.log(error.code); // e.g., "TIMEOUT"
console.log(error.retryable); // boolean
console.log(error.status); // full status object
console.log(error.responseId); // correlation ID
}
}
Middleware-Specific Errors#
CircuitBreakerError- Circuit is openRateLimitError- Rate limit exceeded
Metadata#
Metadata flows through the request/response cycle:
// Request metadata
await client.call(method, payload, {
timeout: { overall: 5000 },
auth: { token: "override" },
custom: { anything: "here" },
});
// Middleware adds to metadata
// - retry: { attempt, maxAttempts }
// - tracing: { traceId, spanId, parentSpanId }
// - pagination: { page, limit }