<!-- Markdown mirror of https://justscale.sh/docs/rpc/client -->

# RPC Client

Type-safe gRPC clients with DI integration

The `defineRpcClient` function creates type-safe gRPC clients that integrate seamlessly with JustScale's dependency injection system. Clients automatically get all methods from the contract with full TypeScript inference.

## Basic Usage

Define a client by specifying the contract and configuration source:

TypeScript

```typescript
import { defineRpcClient } from '@justscale/rpc'
import { defineService } from '@justscale/core'
import { GreeterContract } from './greeter.proto'

// Settings service provides configuration
class GreeterSettings extends defineService({
  inject: {},
  factory: () => ({
    address: 'localhost:50051',
    timeout: 30_000,
  }),
}) {}

// Define the client with injected settings
const GreeterClient = defineRpcClient({
  contract: GreeterContract,
  inject: { settings: GreeterSettings },
})
```

The client looks for a `settings` dependency with an `address` property by default. All methods from the contract become available on the client instance.

## Injecting Clients into Services

Once defined, inject the client into your services like any other dependency:

TypeScript

```typescript
class OrderService extends defineService({
  inject: { greeter: GreeterClient },
  factory: ({ greeter }) => ({
    async processOrder(customerId: string) {
      // Fully typed - TypeScript knows SayHello's input/output
      const reply = await greeter.SayHello({ name: customerId })
      return reply.message
    },

    async *streamUpdates(orderId: string) {
      // Server streaming methods return AsyncIterable
      for await (const update of greeter.StreamUpdates({ orderId })) {
        yield update
      }
    },
  }),
}) {}
```

## Config Factory

For more control over configuration, use the `config` factory function. This lets you combine multiple injected dependencies to build the client config:

TypeScript

```typescript
import { EnvService, ServiceDiscovery } from '@justscale/core'

const GreeterClient = defineRpcClient({
  contract: GreeterContract,
  inject: {
    env: EnvService,
    discovery: ServiceDiscovery,
  },
  config: ({ env, discovery }) => ({
    // Try service discovery first, fall back to env var
    address: discovery.resolve('greeter') ?? env.get('GREETER_ADDR'),
    timeout: Number(env.get('GREETER_TIMEOUT') ?? 30_000),
    metadata: {
      'x-api-key': env.get('GREETER_API_KEY'),
    },
  }),
})
```

The config factory receives all resolved dependencies and must return an `RpcClientConfig` object.

## Retry Configuration

Configure automatic retries with exponential backoff for transient failures:

TypeScript

```typescript
const GreeterClient = defineRpcClient({
  contract: GreeterContract,
  inject: { settings: GreeterSettings },
  config: ({ settings }) => ({
    address: settings.address,
    retry: {
      maxAttempts: 3,           // Total attempts (default: 3)
      initialBackoff: 100,     // Initial delay in ms (default: 100)
      maxBackoff: 10_000,      // Maximum delay in ms (default: 10000)
      backoffMultiplier: 2,    // Multiplier per retry (default: 2)
      retryableStatusCodes: [  // Which codes to retry
        14, // UNAVAILABLE
        8,  // RESOURCE_EXHAUSTED
      ],
    },
  }),
})
```

By default, only `UNAVAILABLE` and `RESOURCE_EXHAUSTED` status codes trigger retries. Failed retries use exponential backoff with jitter to avoid thundering herd.

## Load Balancing

For high availability, configure a resolver and load balancer in the client config:

TypeScript

```typescript
import {
  defineRpcClient,
  staticResolver,
  roundRobinBalancer,
} from '@justscale/rpc'
import { GreeterContract } from './greeter.proto'

const GreeterClient = defineRpcClient({
  contract: GreeterContract,
  inject: {},
  config: () => ({
    // Logical name (passed to resolver)
    address: 'greeter-service',

    // Resolves the address to multiple endpoints
    resolver: staticResolver([
      'server1:50051',
      'server2:50051',
      'server3:50051',
    ]),

    // Distributes requests across endpoints
    loadBalancer: roundRobinBalancer(),
  }),
})
```

Available load balancers:

TypeScript

```typescript
import {
  roundRobinBalancer,
  randomBalancer,
  pickFirstBalancer,
  weightedRoundRobinBalancer,
} from '@justscale/rpc'

// Round-robin: cycle through addresses in order
roundRobinBalancer()

// Random: pick randomly with equal probability
randomBalancer()

// Pick-first: always use first address, failover on error
pickFirstBalancer()

// Weighted: distribute based on capacity
weightedRoundRobinBalancer(new Map([
  ['server1:50051', 3],  // Gets 3x traffic
  ['server2:50051', 1],  // Gets 1x traffic
]))
```

Available resolvers and custom resolver example:

TypeScript

```typescript
import { staticResolver, passthroughResolver } from '@justscale/rpc'

// Static list of addresses
staticResolver(['server1:50051', 'server2:50051'])

// Pass-through for single server (uses address directly)
passthroughResolver()

// Custom DNS resolver
const dnsResolver = {
  async resolve(target: string) {
    const addresses = await dns.resolve4(target)
    return addresses.map(ip => `${ip}:50051`)
  },
}
```

Load balancers receive success/failure feedback after each request, allowing intelligent routing decisions. The client maintains a connection pool, reusing HTTP/2 sessions for each endpoint.

## Full Configuration Reference

All available options for `RpcClientConfig`:

TypeScript

```typescript
interface RpcClientConfig {
  // Server address (host:port) or logical name for resolver
  address: string

  // TLS configuration
  tls?: {
    ca?: Buffer | string       // CA certificate
    cert?: Buffer | string     // Client certificate
    key?: Buffer | string      // Client private key
    insecure?: boolean         // Skip verification (dev only!)
  }

  // Request timeout in ms (default: 30000)
  timeout?: number

  // Default metadata sent with every request
  metadata?: Record<string, string>

  // Retry configuration
  retry?: {
    maxAttempts?: number           // default: 3
    initialBackoff?: number        // default: 100ms
    maxBackoff?: number            // default: 10000ms
    backoffMultiplier?: number     // default: 2
    retryableStatusCodes?: number[] // default: [UNAVAILABLE, RESOURCE_EXHAUSTED]
  }

  // Request compression (identity, gzip, deflate, none)
  compression?: 'identity' | 'gzip' | 'deflate' | 'none'

  // Message size limits (default: 4MB)
  maxReceiveMessageSize?: number
  maxSendMessageSize?: number

  // Address resolver for multi-server deployments
  resolver?: AddressResolver

  // Load balancer for distributing requests (default: round-robin)
  loadBalancer?: LoadBalancer
}
```

## Per-Call Options

Override client defaults on a per-call basis:

TypeScript

```typescript
// Override timeout for a slow operation
const reply = await greeter.SayHello(
  { name: 'World' },
  { timeout: 60_000 }
)

// Add request-specific metadata
const reply = await greeter.SayHello(
  { name: 'World' },
  { metadata: { 'x-request-id': requestId } }
)

// Support cancellation via AbortSignal
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)

const reply = await greeter.SayHello(
  { name: 'World' },
  { signal: controller.signal }
)
```

## Closing Connections

Clean up client connections when done:

TypeScript

```typescript
// The client instance has a close() method
await greeter.close()

// In a service with cleanup
class OrderService extends defineService({
  inject: { greeter: GreeterClient },
  factory: ({ greeter }) => ({
    // ... methods ...

    async [Symbol.asyncDispose]() {
      await greeter.close()
    },
  }),
}) {}
```

## Next Steps

- RPC Overview
- RPC Controllers
- Status Codes & Errors
