Custom Executor
Any HTTP client works with routar. Use createExecutor to wrap it.
Basic example
import { createExecutor, serializeParams } from '@routar/core'
const executor = createExecutor(async ({ method, url, params, body, headers, signal }) => {
const fullURL = new URL(url)
if (params) {
serializeParams(params).forEach((v, k) => fullURL.searchParams.set(k, v))
}
const res = await fetch(fullURL.toString(), {
method,
body: body != null ? JSON.stringify(body) : undefined,
headers: { ...(body != null ? { 'Content-Type': 'application/json' } : {}), ...headers },
signal,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})params holds the query fields from your endpoint’s request schema. Use serializeParams from @routar/core to turn them into a URLSearchParams-compatible string.
With ky
import ky from 'ky'
import { createExecutor } from '@routar/core'
const executor = createExecutor(async ({ method, url, params, body, headers, signal }) => {
return ky(url, { method, json: body, searchParams: params, headers, signal }).json()
})With got
import got from 'got'
import { createExecutor } from '@routar/core'
const executor = createExecutor(async ({ method, url, params, body, headers, signal }) => {
const { body: data } = await got(url, {
method,
json: body,
searchParams: params,
headers,
signal,
responseType: 'json',
})
return data
})Adding plugins
Pass plugins via the options object. They apply to all requests through this executor.
import { createExecutor, definePlugin, logger } from '@routar/core'
const authPlugin = definePlugin({
name: 'auth',
onRequest: async (opts) => ({
...opts,
headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },
}),
})
const executor = createExecutor(transport, {
plugins: [authPlugin, logger()],
})For retry and timeout on a custom transport, configure them on the underlying HTTP client (e.g. ky.create({ retry, timeout }), got({ retry, timeout })). The built-in retry and timeout options are only available on createFetchExecutor.
Dynamic Authorization header
The most common custom executor use case is injecting auth tokens per request:
import { createExecutor, serializeParams } from '@routar/core'
const executor = createExecutor(async ({ method, url, params, body, headers, signal }) => {
const token = await getAccessToken()
const fullURL = new URL(url)
if (params) {
serializeParams(params).forEach((v, k) => fullURL.searchParams.set(k, v))
}
const res = await fetch(fullURL.toString(), {
method,
body: body != null ? JSON.stringify(body) : undefined,
headers: {
...(body != null ? { 'Content-Type': 'application/json' } : {}),
Authorization: `Bearer ${token}`,
...headers,
},
signal,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})For per-request auth headers in SSR, prefer the defaultHeaders option on createFetchExecutor or the factory form of createAxiosExecutor — both are called on every request.
Validator compatibility
The response schema only needs a .parse(data: unknown): T method. You can use Zod, Valibot, Yup, or a hand-rolled validator:
// hand-rolled validator
const TodoValidator = {
parse(data: unknown): Todo {
if (typeof data !== 'object' || data === null) throw new Error('invalid')
return data as Todo
},
}
endpoint({ method: 'GET', path: '/', response: TodoValidator })