createApi(executor, router)
Produces a fully-typed API client. Each endpoint becomes an async function with the signature (params, signalOrOptions?) => Promise<Response> — the second argument is an AbortSignal or a { signal?, headers?, timeout? } options object (see Per-call options).
import { createApi } from '@routar/core'
const todoApi = createApi(executor, todoRouter)
// params follow { path?, query?, body? }
await todoApi.getDetail({ path: { id: 1 } })
await todoApi.create({ body: { title: 'buy milk' } })
await todoApi.update({ path: { id: 1 }, body: { completed: true }, query: { version: 2 } })Overloads
| Form | Usage |
|---|---|
createApi(executor, router) | Pre-built router via defineRouter — preferred for multi-environment setups |
createApi(executor, prefix, endpoints) | Inline endpoints with a URL prefix |
createApi(executor, endpoints) | Inline endpoints without a prefix — simplest form for small APIs |
// with a pre-built router
createApi(executor, todoRouter)
// inline with prefix
createApi(executor, '/todos', { getList: endpoint({ ... }) })
// inline without prefix
createApi(executor, { getList: endpoint({ ... }) })Parameters
| Parameter | Type | Description |
|---|---|---|
executor | Executor | HTTP transport. Create with createFetchExecutor or createAxiosExecutor. |
router | RouterDef | RouterEndpoints | Router definition or inline endpoints record |
prefix | string (optional) | URL prefix when passing endpoints inline |
Request params
Request params map to { path?, query?, body? } matching the request schema shape:
// request: { path: z.object({ id: z.number() }) }
await api.getDetail({ path: { id: 1 } })
// request: { query: z.object({ page: z.number() }) }
await api.getList({ query: { page: 2 } })
// request: { body: z.object({ title: z.string() }) }
await api.create({ body: { title: 'buy milk' } })Per-call options
The second argument accepts either a bare AbortSignal (backward compatible) or an options object { signal?, headers?, timeout? }:
// cancel with an AbortSignal
const controller = new AbortController()
await todoApi.getList({}, controller.signal)
controller.abort()
// per-call headers + timeout
await todoApi.create(
{ body: { title: 'buy milk' } },
{
headers: { 'Idempotency-Key': key }, // merged over executor defaults
timeout: 30_000, // throws TimeoutError when exceeded
signal: controller.signal,
},
)headersare seeded onto the request and merged over the executor’s default headers (per-call wins). A plugin’sonRequestruns afterward and wins on a key collision (plugins are cross-cutting policy such as auth).timeoutaborts the request with aTimeoutErrorand is applied by the core client, so it works on every executor (fetch, Axios, ky, custom) and composes with any executor-level timeout (whichever fires first).
Validation modes
validate controls whether request/response schemas run, per phase. Each phase accepts true (default — validate and throw on failure), false (skip), or 'warn':
// observe response drift without breaking production
const api = createApi(executor, todoRouter, {
validate: { request: true, response: 'warn' },
onValidationError: (err, ctx) => {
// ctx: { kind: 'request' | 'response', method, url, data }
Sentry.captureException(err, { extra: ctx })
},
})In 'warn' mode a parse failure passes the raw data through (no outage) and calls onValidationError(error, context) instead of throwing — the drift-observation middle ground between full validation and validate: false. onValidationError is never called under true (which throws) or false (which skips).
SSR/CSR pattern
The typical pattern is to create two clients from the same router — one for CSR, one for SSR:
export const todoApi = createApi(clientExecutor, todoRouter) // CSR
export const todoServerApi = createApi(serverExecutor, todoRouter) // SSRSee the SSR/CSR guide for a complete example.