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.
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) β
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 β
// 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.
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:
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:
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:
// 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:
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.