Skip to Content
GuidesCustom Executor

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 })
Last updated on