api
@mrzr/api-client
Requests

Request Config

Per-request configuration options and overrides

Every option accepted as the last argument of a request method. All are optional, and all override their client-wide equivalent.

await api.get<User>("/users/{id}", { /* RequestConfig<User> */ });

Full table

OptionTypeDefaultPurpose
URL
baseUrlstringclient baseUrlOverride the base for this call
addToUrl(string | number)[]Append path segments, with a trailing slash
addTemplateToUrlRecord<string, string | number>Replace {key} placeholders
paramsParams<T>Query string; nested objects and arrays supported
Headers & body
headersRecord<string, string>Merged over the client headers
stringifyBodybooleantruefalse passes the body to fetch untouched
isFormDatabooleanautoForce multipart handling (drops Content-Type)
duplex"half""half"Set automatically for ReadableStream bodies
Timing
timeoutnumber30000Per-attempt timeout in ms; 0 disables
signalAbortSignalYour cancellation signal, merged with the timeout
Cancellation
cancelablebooleanclient settingTrack this request so cancel() can stop it
cancelKeystringA stable identity: cancel by name, and the unit takeLatest compares
cancelGroupstring | string[]Tags for bulk cancellation
takeLatestbooleanfalseSupersede the previous request with the same identity
throwOnCancelbooleanfalseReject instead of resolving when canceled
Auth
skipAuthbooleanfalseSend without Authorization, skip proactive refresh
refreshTokenCheckbooleantruefalse disables the 401 → refresh → retry flow
uploadSkewMsnumberRefresh before sending if the token dies within this window
Response shaping
fullDatabooleanfalseReturn the raw body instead of unwrapping { data }
beforeSelectOptions(data: T) => unknownTransform the payload; runs before afterFunc
afterFunc(data: T) => unknownTransform the payload
beforeFunc(body: unknown) => unknownTransform the outgoing body
Errors & logging
throwErrorbooleanclient settingReject with ApiError, or resolve with the envelope
hideErrorMessagebooleanfalseSuppress the client's onError callback
logbooleanfalseEmit a structured log entry
Native fetch
cacheRequestCacheForwarded to fetch
integritystringForwarded to fetch
keepalivebooleanForwarded to fetch
modeRequestModeForwarded to fetch
redirectRequestRedirectForwarded to fetch
referrerstringForwarded to fetch
referrerPolicyReferrerPolicyForwarded to fetch

Everything at once

await api.get<User>("/users/{id}", {
  addTemplateToUrl: { id: 42 },        // /users/42
  addToUrl: ["posts", 7],              // /users/42/posts/7/
  params: { page: 1, filter: { active: true }, tags: ["a", "b"] },
  headers: { "X-Trace": "abc" },
  timeout: 5_000,
  baseUrl: "https://other.example.com",
  skipAuth: true,
  refreshTokenCheck: false,
  fullData: true,
  hideErrorMessage: true,
  throwError: false,
  uploadSkewMs: 600_000,
  duplex: "half",
  log: true,
  signal: controller.signal,
  cancelable: true,
  cancelKey: "product-detail",
  cancelGroup: "product-modal",
  takeLatest: true,
  cache: "no-store",
  beforeFunc: (body) => body,
  afterFunc: (data) => data,
});

Option notes

fullData

Most APIs wrap payloads in { message, data }. The client lifts data out automatically. fullData: true gives you the untouched body.

// server: { "message": "ok", "data": { "id": 1 }, "meta": { "total": 99 } }

const a = await api.get("/users/1");
a.data; // { id: 1 }

const b = await api.get("/users/1", { fullData: true });
b.data; // { message: "ok", data: { id: 1 }, meta: { total: 99 } }

Reach for it when you need sibling fields like meta or links.

login() uses fullData: true internally so it can find tokens anywhere in the response, then re-applies your unwrapping preference to the value it returns.

refreshTokenCheck: false

Disables both halves of the refresh machinery for this call: no proactive refresh, and a 401 is returned as-is instead of triggering refresh-and-retry.

Use it for your own auth endpoints, so a failing refresh can't recurse.

await api.post("/auth/verify-otp", { code }, { refreshTokenCheck: false });

A per-request uploadSkewMs still forces a refresh even when the client-wide skew is 0, because it expresses "this specific request needs a fresh token".

hideErrorMessage

Only suppresses the client's onError callback. It does not stop throwError from rejecting, and it does not change the envelope. Use it when a failure is expected and shouldn't produce a toast.

const res = await api.get("/feature-flags", {
  hideErrorMessage: true,
  throwError: false,
});
const flags = res.status ? res.data : defaults;

throwError

Per-request always beats client-wide, in both directions:

const api = createClient({ throwError: false });   // envelope by default
await api.get("/a");                        // resolves with the envelope
await api.get("/b", { throwError: true });  // rejects with ApiError

const strict = createClient();              // throws by default
await strict.get("/c");                     // rejects
await strict.get("/d", { throwError: false }); // resolves with the envelope

log

await api.get("/users", { log: true });

Emits a LogEntry to the client's onLog handler, or console.info("[api-client]", entry) if none is set. Logged for both success and failure. See [[Logging and Observability]].

signal

Combined with the internal timeout signal and the client's cancel registry — whichever fires first wins. Aborting yields statusCode: 0 with canceled: true and message "Request aborted"; a timeout yields 408, "Request timed out" and leaves canceled unset, so you can tell them apart.

cancelable, cancelKey, cancelGroup, takeLatest

These drive api.cancel(). Cancellation is off until the client enables it, and even then covers only GET — but any single request can opt in or out:

await api.post("/draft", body, { cancelable: true });   // opt a write in
await api.get("/session", { cancelable: false });       // opt a read out

cancelKey and takeLatest both imply cancelable. A cancelGroup alone only tags the request, so tagging a write never makes it cancelable behind your back.

// Stale-search: each keystroke retires the last
await api.get("/search", { params: { q }, cancelKey: "search", takeLatest: true });

// Bulk: one call kills them all
await api.get("/a", { cancelGroup: "checkout", cancelable: true });
api.cancel("checkout");

See [[Cancellation]] for the full picture.

throwOnCancel

Independent of throwError, and false by default: a canceled request resolves with canceled: true even on a throwing client. Real failures still throw.

That is deliberate — rejecting makes TanStack Query retry the request you just canceled, and turns the ordinary useEffect async pattern into an unhandled rejection. See [[Cancellation]].

await api.get("/products", { throwOnCancel: true });   // opt back in

stringifyBody and isFormData

Both are escape hatches; detection is automatic for all standard body types.

  • stringifyBody: false → pass the body to fetch untouched.
  • isFormData: true → treat the body as multipart and drop the Content-Type header so the runtime can generate the boundary.

Use them for exotic body objects the detector can't recognise (a polyfilled FormData, for example).

Next: [[Responses and Errors]]

On this page