Skip to Content
GuidesArchitecture

Architecture

routar structures an HTTP client as five discrete layers. Each layer has one responsibility; layers below don’t know about layers above.

endpoint() / defineRouter() ← Layer 1: Spec declaration createApi() ← Layer 2: Typed client factory createExecutor() + plugins ← Layer 3: Middleware chain transport function ← Layer 4: HTTP I/O (fetch / axios / ky) createQueries() ← Layer 5: TanStack Query bindings (optional)

Layer 1 — Spec declaration

endpoint() and defineRouter() declare the HTTP contract as a pure value. No network access happens here; this layer defines what an API call is.

const todoRouter = defineRouter('/todos', { getDetail: endpoint({ method: 'GET', path: '/:id', request: { path: z.object({ id: z.number() }) }, response: TodoSchema, }), })

The request schema separates path, query, and body explicitly — the contract is visible in the type, not hidden in a comment. The response schema stays a plain ZodObject so it can be composed and shared across endpoints.

See: endpoint() · defineRouter()

Layer 2 — Typed client factory

createApi(executor, router) binds a router to a transport and returns a typed client. When you call a method on the client, five things happen in order:

  1. request.parse(params) — validates and narrows the input
  2. resolvePath(prefix + path, pathParams) — substitutes :param segments
  3. executor.execute(opts) — dispatches the HTTP request
  4. response.parse(raw) — validates the raw response
  5. adapter(validated) — transforms the validated value (if defined)
const todoApi = createApi(fetchExecutor, todoRouter) // validate → resolve path → execute → validate → adapt const todo = await todoApi.getDetail({ path: { id: 1 } })

See: createApi()

Layer 3 — Middleware chain

createExecutor(transportFn, { plugins }) wraps a transport function with a plugin chain composed via reduceRight — the first plugin in the array is outermost.

Each plugin can hook into three points:

HookRunsUse for
onRequestBefore the transportAuth headers, logging, timeouts
onResponseAfter a successful responseResponse logging, cache writes
onErrorOn transport errorRetry logic, error normalization
const executor = createFetchExecutor(baseURL, { plugins: [logger(), withTimeout(5000)], })

See: createExecutor() · Plugins

Layer 4 — Transport

The transport function is the only layer that performs actual HTTP I/O. routar ships three transport packages so you only include what you need:

PackageTransportUse when
@routar/corecreateFetchExecutorNative fetch (SSR, edge, tests)
@routar/axioscreateAxiosExecutorAxios (interceptors, progress)
@routar/kycreateKyExecutorky (lightweight fetch wrapper)

Because the transport is injected at createApi(executor, router), the same router runs on any transport — no duplication between SSR and CSR.

See: Executors

Layer 5 — TanStack Query bindings (optional)

createQueries(api) reads the $router metadata stamped on the client and generates typed queryOptions / mutationOptions accessors and query key helpers.

export const todoQuery = createQueries(todoApi) // queryOptions for GET endpoints const options = todoQuery.getDetail({ path: { id: 1 } }) // options.queryKey → [..., 'getDetail', { path: { id: 1 } }] // options.queryFn → () => todoApi.getDetail(...)

This layer is entirely optional — if you’re not using TanStack Query, layers 1–4 are everything you need.

See: createQueries() · TanStack Query guide

Putting it together

A complete setup wires all layers in one file:

// remote/services/todo.ts const todoRouter = defineRouter('/todos', { // Layer 1 — spec getList: endpoint({ ... }), getDetail: endpoint({ ... }), }) const todoApi = createApi(fetchExecutor, todoRouter) // Layers 2–4 — client export const todoQuery = createQueries(todoApi) // Layer 5 — query bindings export type TodoApiTypes = ApiTypes<typeof todoApi>

Each layer has exactly one reason to change:

What changesWhere to change
Endpoint path, method, or schemaLayer 1 — endpoint()
Validation or response transformLayer 1 — request / response / adapter
Auth headers, retry, timeoutLayer 3 — plugins
Switch from fetch to axiosLayer 4 — swap the executor
Query key shape or per-endpoint defaultsLayer 5 — createQueries() options
Last updated on