Skip to content

CreateFetcherOptions

function createFetcher<TSchema extends Schema, TParsedError = unknown>(
schema: TSchema,
options: CreateFetcherOptions<TParsedError>,
): FetcherInstance<TSchema, TParsedError>;
interface CreateFetcherOptions<TParsedError = unknown> {
baseUrl: string;
fetchFn?: typeof fetch;
defaultHeaders?:
| Record<string, string>
| (() => Record<string, string> | Promise<Record<string, string>>);
parseError?: (
res: Response,
body: unknown,
) => TParsedError | Promise<TParsedError>;
onError?: (
err: ApiError<TParsedError>,
ctx: ErrorContext,
) => void;
}
interface ErrorContext {
routeName: string;
method: string;
url: string;
status: number; // 0 for network errors
}
TypeDefaultRequired
stringnoneyes

Prepended to every request path. Trailing slashes are normalized, so both of these work:

createFetcher(schema, { baseUrl: "https://api.example.com" });
createFetcher(schema, { baseUrl: "https://api.example.com/" });

You can include a path prefix:

createFetcher(schema, { baseUrl: "https://api.example.com/v1" });
// Route path "/posts" → "https://api.example.com/v1/posts"
TypeDefault
typeof fetchglobalThis.fetch

A custom fetch implementation. Common use cases:

// Node 16 / Bun / Deno / Workers
import { fetch as undiciFetch } from "undici";
createFetcher(schema, { baseUrl: "", fetchFn: undiciFetch });
// Wrapping with retry middleware
const wrappedFetch: typeof fetch = async (input, init) => {
for (let i = 0; i < 3; i++) {
const res = await fetch(input, init);
if (res.status < 500) return res;
}
return fetch(input, init);
};
createFetcher(schema, { baseUrl: "", fetchFn: wrappedFetch });
TypeDefault
Record<string, string> | () => Record<string, string> | Promise<Record<string, string>>undefined

Headers applied to every request. Two shapes:

createFetcher(schema, {
baseUrl: "",
defaultHeaders: {
"x-api-version": "2026-01-01",
"x-client": "web",
},
});

Resolved once at construction time.

createFetcher(schema, {
baseUrl: "",
defaultHeaders: async () => ({
Authorization: `Bearer ${await getAccessToken()}`,
"x-tenant-id": currentTenantId(),
}),
});

Called on every fetcher.fetch() invocation, awaited if it returns a Promise. Useful for token refresh.

Per-call headers always win over defaultHeaders:

fetcher.fetch("getPost", {
pathParams: { facilityId, postId },
headers: { "x-debug": "1" }, // merged with / overrides defaults
});
TypeDefault
(res: Response, body: unknown) => TParsedError | Promise<TParsedError>uses raw body as parsed

Runs for any non-2xx response. Receives the raw Response and parsed JSON body (or null if the body isn’t JSON). Return the parsed-error shape you want to expose on ApiError.parsed:

interface ApiProblem {
type: string;
title: string;
detail: string;
}
createFetcher<typeof schema, ApiProblem>(schema, {
baseUrl: "",
parseError: async (res) => {
const body = (await res.json().catch(() => null)) as ApiProblem | null;
return body ?? { type: "about:blank", title: res.statusText, detail: "" };
},
});

Without parseError, ApiError.parsed is the raw JSON body (typed as unknown).

TypeDefault
(err: ApiError<TParsedError>, ctx: ErrorContext) => voidundefined

A side-effecting hook fired for every failure (HTTP error or network error). Use it for telemetry and cross-cutting auth:

import { isApiError } from "@use-q/api-client";
import * as Sentry from "@sentry/browser";
createFetcher(schema, {
baseUrl: "",
onError: (err, ctx) => {
if (err.status === 401) {
tokenStore.clear();
window.location.assign("/login");
return;
}
Sentry.captureException(err, {
tags: {
route: ctx.routeName,
method: ctx.method,
status: String(ctx.status),
},
extra: { url: ctx.url },
});
},
});

ErrorContext.status is 0 for network errors (no response received).

onError is fire-and-forget — its return value is ignored and exceptions thrown inside it are swallowed.

createFetcher<TSchema, TParsedError = unknown>(schema, options);
  • TSchema is the schema type. Pass it as typeof schema to get full inference at call sites.
  • TParsedError is the shape returned by parseError. Default: unknown (the raw body).
createFetcher<typeof schema, ApiProblem>(schema, {
baseUrl: "",
parseError: async (res) => /* … */,
});
interface FetcherInstance<TSchema extends Schema, TParsedError = unknown> {
fetch<TRoute extends RouteKey<TSchema>>(
routeName: TRoute,
options: RouteParams<TSchema, TRoute> & {
headers?: Record<string, string>;
signal?: AbortSignal;
},
): Promise<RouteResponse<TSchema, TRoute>>;
}

See createFetcher for usage recipes.