kibinrpc

Client

Type-safe fetch client with automatic batching, retry, and interceptors.

Installation

npm install @kibinrpc/client

createKibinClient

import { createKibinClient } from '@kibinrpc/client'
import type { AppRouter } from './server/router'

const client = createKibinClient<AppRouter>({
  baseUrl: '/api/rpc',
})

The generic parameter AppRouter is the type of your server's router (typeof router). All method signatures, parameters, and return types are inferred from it.

Making calls

// Return type inferred from the server
const user = await client.user.getUser('1')    // Promise<User>
const posts = await client.post.listPosts()    // Promise<Post[]>

Automatic batching

Calls that happen in the same JavaScript tick are automatically coalesced into a single HTTP request:

// Both happen in the same tick → one batched request
const [users, posts] = await Promise.all([
  client.user.listUsers(),
  client.post.listPosts(),
])

// Sequential awaits → two separate requests
const user = await client.user.getUser('1')
const posts = await client.post.listPosts()

No configuration is required. The server receives either a single object or an array and handles both automatically.

Bypassing the batch queue

Use $unbatched to send a call as a standalone HTTP request, skipping the microtask queue entirely:

// Sent immediately as a single request — never grouped with other calls
const user = await client.$unbatched.user.getUser('1')

This is useful when you need guaranteed ordering, want to avoid a call being grouped with others, or are testing individual actions in isolation.

$unbatched has the same type signature as the regular client, so no other code changes are needed.

Configuration

const client = createKibinClient<AppRouter>({
  baseUrl: '/api/rpc',

  // Static headers sent with every request
  headers: {
    'X-App-Version': '1.0.0',
  },

  // Per-attempt timeout in ms (uses AbortSignal.timeout internally)
  timeout: 10_000,

  // AbortSignal to cancel all requests from this client
  signal: controller.signal,

  // Retry on network errors and 5xx responses
  retry: {
    attempts: 3,   // total attempts (default: 3)
    delay: 300,    // base delay in ms, doubles each retry (default: 300)
  },

  // See /docs/interceptors
  interceptors: {
    request: (ctx) => ctx,
    response: (ctx) => ctx.data,
    error: (ctx) => { throw ctx.error },
  },
})

Timeout and cancellation

timeout

Set a per-attempt time limit in milliseconds. Each retry gets its own fresh timer:

const client = createKibinClient<AppRouter>({
  baseUrl: '/api/rpc',
  timeout: 5_000, // each attempt must complete within 5 s
})

If an attempt exceeds the limit, it throws a KibinError with code TIMEOUT and does not retry.

signal

Pass an AbortSignal to cancel all requests from this client at once — useful for component unmount, navigation, or user-triggered cancellation:

const controller = new AbortController()

const client = createKibinClient<AppRouter>({
  baseUrl: '/api/rpc',
  signal: controller.signal,
})

// Later — cancels any in-flight request
controller.abort()

When the signal fires, pending requests reject with KibinError code ABORTED.

Both options can be combined — whichever fires first wins:

createKibinClient<AppRouter>({
  baseUrl: '/api/rpc',
  timeout: 5_000,
  signal: controller.signal, // aborts if component unmounts before the timeout
})

Dynamic headers

headers accepts a function (sync or async) that is called once per fetch attempt. This is the idiomatic way to attach tokens that may change between retries:

const client = createKibinClient<AppRouter>({
  baseUrl: '/api/rpc',
  headers: async () => ({
    Authorization: `Bearer ${await getAccessToken()}`,
  }),
})

The function is called fresh on every attempt, so a token refreshed during a retry is always current.

Retry behaviour

Failed requests are retried with exponential backoff. The delay between attempts doubles each time: delay, delay × 2, delay × 4, etc.

ScenarioSingle callBatched call
Network errorretry all attemptsretry whole batch
HTTP 5xxretry all attemptsretry only the failed items
HTTP 4xxno retry, throw immediatelyno retry, reject that item

This means a partial batch failure (e.g. one 404, one 500) retries only the 500 item, while the 404 is immediately rejected.

isKibinError

Type guard for structured server errors:

import { isKibinError } from '@kibinrpc/client'

try {
  await client.user.getUser('999')
} catch (err) {
  if (isKibinError(err)) {
    console.log(err.code)    // e.g. 'NOT_FOUND'
    console.log(err.message) // e.g. 'User not found'
  }
}

Exported types

import type {
  KibinClient,
  KibinClientConfig,
  ClientInterceptors,
  RequestCtx,
  ResponseCtx,
  ErrorCtx,
  RetryConfig,
} from '@kibinrpc/client'

On this page