use-q

Command Palette

Search for a command to run...

CreateFetcherOptions

Every option accepted by createFetcher — type, default, and example.

function createFetcher(options: CreateFetcherOptions): FetcherInstance;
 
interface CreateFetcherOptions {
  baseUrl: string;
  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
  fetch?: typeof fetch;
  parseError?: (input: { response: Response; data: unknown }) => unknown;
  onError?: (error: unknown) => void;
}

Fields

baseUrl

TypeDefaultRequired
stringnoneyes

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

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

You can include a path prefix:

createFetcher({ baseUrl: "https://api.example.com/v1" });
// Path "/posts" → "https://api.example.com/v1/posts"

Paths that are already absolute http(s):// URLs bypass baseUrl joining entirely.

fetch

TypeDefault
typeof fetchglobalThis.fetch

A custom fetch implementation. Common use cases:

// Alternative fetch implementations (undici, polyfills, test doubles)
import { fetch as undiciFetch } from "undici";
createFetcher({ baseUrl: "…", fetch: 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({ baseUrl: "…", fetch: wrappedFetch });

headers

TypeDefault
HeadersInit | () => HeadersInit | Promise<HeadersInit>undefined

Headers applied to every request. Any HeadersInit works — a plain object, a Headers instance, or an array of [key, value] entries. Two shapes:

Static

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

Function (sync or async)

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

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

Per-call headers always win over the constructor-level headers:

fetcher.fetch("/facilities/{facilityId}/posts/{postId}", {
  params: { facilityId, postId },
  headers: { "x-debug": "1" }, // merged with / overrides defaults
});

parseError

TypeDefault
(input: { response: Response; data: unknown }) => unknownundefined (raw body used as data)

Runs for any non-2xx response. Receives a single object with the raw Response and the already-parsed body (data is JSON when the response's content-type is application/json, the raw text otherwise, or undefined for an empty body). The return value becomes ApiError.data:

interface ApiProblem {
  type: string;
  title: string;
  detail: string;
}
 
createFetcher({
  baseUrl: "…",
  parseError: ({ response, data }) => {
    const body = data as Partial<ApiProblem> | null;
    return {
      type: body?.type ?? "about:blank",
      title: body?.title ?? response.statusText,
      detail: body?.detail ?? "",
    } satisfies ApiProblem;
  },
});

Two special cases:

  • Returning an ApiError instance throws that instance as-is (full control over message, subclassing, etc.).
  • Returning undefined/null falls back to the raw parsed body as data.

Without parseError, ApiError.data is the raw parsed body (typed as unknown). Narrow it at catch sites with isApiError<ApiProblem>(err).

onError

TypeDefault
(error: unknown) => voidundefined

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

import { isApiError } from "@use-q/api-client";
import * as Sentry from "@sentry/browser";
 
createFetcher({
  baseUrl: "…",
  onError: (err) => {
    if (isApiError(err) && err.status === 401) {
      tokenStore.clear();
      window.location.assign("/login");
      return;
    }
    Sentry.captureException(err);
  },
});

For HTTP failures, err is the normalized ApiErrorstatus, statusText, url, method, and data are all on the error itself, so no separate context object is needed. For network failures (fetch itself rejecting — DNS, CORS, abort…), err is the raw thrown error and isApiError(err) is false.

onError's return value is ignored; the original error is always thrown afterwards.

Returned shape

interface FetcherInstance {
  baseUrl: string;
  fetch<TResponse = unknown>(
    path: string,
    options?: FetcherFetchOptions,
  ): Promise<TResponse>;
}
 
interface FetcherFetchOptions {
  method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; // default "GET"
  params?: Record<string, string | number | boolean>;
  searchParams?: SearchParams;
  body?: unknown;
  signal?: AbortSignal;
  headers?: HeadersInit;
}

fetch is path-based: {param} placeholders in path are filled from params (URI-encoded, throwing on missing values), searchParams are appended to the URL, and the response type is whatever you pass as the TResponse generic. Schema-typed, route-id-based calls come from createApiClient, which wraps this same fetcher.

See createFetcher for usage recipes.