Skip to content

Custom Mutators (Fetcher) ​

The Mutator defines how your application communicates with the network. It allows you to use any library (Fetch, Axios, Ky) while maintaining full type safety.

The Architect's Rule ​

As the architect of your API client, your primary rule is: The fetcher must return a ResponsePackage (or a structure containing it).

To facilitate this, vRPC provides a special marker called vRPCResponseBundle. This marker represents the union of all possible response variants for an endpoint.


πŸ—οΈ 1. Default Setup (Minimalist) ​

In the default setup (generated via vrpc init), your fetcher is a "naked" return. It returns the ResponsePackage directly, and vRPC applies types automatically.

The Client (rpc.client.ts) ​

We use a ResponseType alias to tell vRPC exactly how we want our data delivered.

typescript
import { vRPCClient, type vRPCResponse, type vRPCResponseBundle } from 'vrpc'

// By default, the response is just the bundle of all variants
type ResponseType = vRPCResponseBundle;

export const api = new vRPCClient({ fetcher })
  as unknown as TypedClient<RpcOperations, vRPCResponse<ResponseType>>

The Fetcher (rpc.mutator.ts) ​

typescript
export const fetcher = async (url: string, init: RequestInit): Promise<ResponsePackage<any>> => {
  const res = await fetch(url, init)
  const data = await res.json()

  if (!res.ok) {
    throw { status: res.status, data }
  }

  return {
    status: res.status,
    data: data,
    headers: res.headers,
    contentType: 'application/json',
  }
}

πŸš€ 2. Advanced Setup (Custom Wrappers) ​

You can wrap the vRPCResponseBundle inside any structure. vRPC is recursiveβ€”it will find the bundle marker no matter how deep you nest it.

Step 1: Update the Alias ​

typescript
// If you want a Result class wrapper:
type ResponseType = Result<vRPCResponseBundle>

// If you want a custom object:
type ResponseType = { payload: vRPCResponseBundle; timestamp: number }

Step 2: Update the Fetcher ​

Ensure your fetcher's return value matches the new ResponseType structure.

typescript
export const fetcher = async (url: string, init: RequestInit) => {
  const res = await fetch(url, init)
  const data = await res.json()

  const pkg = { status: res.status, data, headers: res.headers }

  // Wrap it in your custom structure
  return {
    payload: pkg,
    timestamp: Date.now(),
  }
}

πŸ‘¨β€πŸ³ Recipes ​

Using Axios ​

Axios is a popular choice for its interceptors. Here is how to map it to a vRPC fetcher:

typescript
import axios from 'axios'

export const fetcher = async (url: string, init: RequestInit) => {
  const response = await axios({
    url,
    method: init.method,
    data: init.body,
    headers: init.headers as any,
    validateStatus: () => true, // Let vRPC handle status-based narrowing
  })

  return {
    status: response.status,
    data: response.data,
    headers: response.headers as any,
    contentType: response.headers['content-type'],
  }
}

Using Ky ​

Ky is a modern, lightweight fetch wrapper that works beautifully with vRPC:

typescript
import ky from 'ky'

export const fetcher = async (url: string, init: RequestInit) => {
  const response = await ky(url, {
    method: init.method,
    body: init.body,
    headers: init.headers,
    throwHttpErrors: false, // Let vRPC handle status-based narrowing
  })

  return {
    status: response.status,
    data: await response.json(),
    headers: response.headers,
    contentType: response.headers.get('content-type') || 'application/json',
  }
}

Implementing a Result Class ​

If you prefer the functional "Result" pattern (success/error objects) over try/catch:

typescript
// 1. Define the Result class
export class Result<T> {
  constructor(
    public readonly success?: T,
    public readonly error?: any,
  ) {}
  static ok<T>(data: T) {
    return new Result(data)
  }
  static fail(err: any) {
    return new Result(undefined, err)
  }
}

// 2. Wrap the bundle in your client
type ResponseType = Result<vRPCResponseBundle>

// 3. Implement the fetcher
export const fetcher = async (url: string, init: RequestInit) => {
  try {
    const res = await fetch(url, init)
    const data = await res.json()
    const pkg = { status: res.status, data, headers: res.headers }

    return res.ok ? Result.ok(pkg) : Result.fail(data)
  } catch (err) {
    return Result.fail(err)
  }
}

πŸ”Œ Serialization ​

If your API requires specific body serialization (e.g., application/x-www-form-urlencoded), you can register serializers in the client:

typescript
import { jsonSerializer, formSerializer } from 'vrpc/serializers'

export const api = new vRPCClient({
  fetcher,
  serializers: {
    'application/json': jsonSerializer,
    'application/x-www-form-urlencoded': formSerializer,
  },
})

NOTE

vRPC is Zero-Runtime. All the logic of parameter mapping and serialization happens inside the compiler plugin during your build process.

Released under the MIT License.