api
@mrzr/api-client
Reference

API Reference

All methods, properties, and exports of @mrzr/api-client

Everything the package exports.

import {
  createClient,
  ApiError,
  buildQueryString,
  getTokenExpiry,
  isTokenExpired,
  MemoryStorage,
  WebStorage,
  CookieStorage,
} from "@mrzr/api-client";

import type {
  ApiClient, AuthMode, AuthState, CancelMatch, CancelOptions, CancelScope,
  CancelSelector, ClientOptions, HttpMethod, IRes, ListResponse, LogEntry,
  Ordering, Params, PendingRequest, RequestConfig, StorageKind,
  TokenExtractor, TokenPair, TokenStorage,
} from "@mrzr/api-client";

createClient(options?): ApiClient

Creates a client. Picks worker mode when the environment allows it, otherwise runs the identical implementation inline.

function createClient(options?: ClientOptions): ApiClient;

Create one per API and export it. Every client owns a worker and a BroadcastChannel.


ApiClient

Request methods

get<R = unknown>(url: string, config?: RequestConfig<R>): Promise<IRes<R>>;
post<R = unknown>(url: string, body?: unknown, config?: RequestConfig<R>): Promise<IRes<R>>;
put<R = unknown>(url: string, body?: unknown, config?: RequestConfig<R>): Promise<IRes<R>>;
patch<R = unknown>(url: string, body?: unknown, config?: RequestConfig<R>): Promise<IRes<R>>;
delete<R = unknown>(url: string, config?: RequestConfig<R>): Promise<IRes<R>>;

R is the type of res.data after unwrapping. Rejects with ApiError on failure unless throwError is false.

const { data } = await api.get<User[]>("/users", { params: { page: 1 } });
const created  = await api.post<User>("/users", { name: "Ada" });
await api.delete("/users/{id}", { addTemplateToUrl: { id: 42 } });

login(body, config?)

login<R = unknown>(body: unknown, config?: RequestConfig<R>): Promise<IRes<R>>;

POSTs to loginUrl with skipAuth, no refresh check, and fullData internally; extracts tokens and user; broadcasts login to other tabs; then re-applies your unwrapping preference to the returned data.

await api.login({ email: "a@b.com", password: "secret" });

Obeys throwError — bad credentials reject with an ApiError.


logout(config?)

logout<R = unknown>(config?: RequestConfig<R>): Promise<IRes<R>>;

POSTs to logoutUrl, then clears tokens locally regardless of the result and broadcasts logout. Never throws — a network failure still signs you out.

await api.logout();

setTokens(tokens)

setTokens(tokens: TokenPair): Promise<void>;

Seed tokens from SSR, an OAuth callback, or your own login flow. Only the keys you pass are updated. Expiry is derived from the JWT exp claim when expiresAt is omitted. Broadcasts login.

await api.setTokens({ accessToken, refreshToken });
await api.setTokens({ accessToken });                 // keeps the existing refresh token
await api.setTokens({ accessToken: undefined });      // local sign-out

refresh()

refresh(): Promise<string | null>;

Forces a refresh. Returns the new access token ("" in cookie mode) or null on failure. Coalesced with any in-flight refresh, so concurrent calls are safe.

const token = await api.refresh();
if (token === null) redirectToLogin();

getAuthState()

getAuthState(): Promise<AuthState>;

Awaits storage hydration, then returns the current state. Never contains tokens.

const { isAuthenticated, expiresAt, user } = await api.getAuthState();

restoreSession(url?)

restoreSession(url?: string): Promise<AuthState>;

Asks the server whether a session already exists, and records the answer.

Needed for authMode: "cookie": httpOnly cookies are unreadable from JS, so after a page reload the client cannot tell a signed-in visitor from a signed-out one until it makes a request.

// On app startup
const state = await api.restoreSession("/api/auth/me");
  • With url, it calls that endpoint and also populates state.user from the response.
  • Without url, it probes the refresh endpoint.
  • In header mode it makes no request and simply returns the rehydrated state, so it is safe to call unconditionally.

onAuthStateChange(listener)

onAuthStateChange(listener: (state: AuthState) => void): () => void;

Subscribe to auth changes. Returns an unsubscribe function. Fires on login, logout, refresh, setTokens, user updates, and cross-tab events. Listener exceptions are caught.

useEffect(() => api.onAuthStateChange(setState), []);

cancel(selector?, reason?)

cancel(selector?: CancelSelector, reason?: string): number;

Cancels matching in-flight requests and returns how many were stopped. Requires the cancel option, or a per-request cancelable — only tracked requests can be canceled.

api.cancel();                                     // everything in flight
api.cancel("/api/v1/products");                   // URL pattern, key or group
api.cancel(/\/products\/\d+$/);                    // regex
api.cancel({ url: "/users/:id", method: "GET" }); // all fields must match
api.cancel((r) => Date.now() - r.startedAt > 10_000);
api.cancel("/api/v1/products", "left the page");  // with a reason

Canceled requests reject with an ApiError carrying canceled: true, or resolve with { canceled: true, statusCode: 0 } when throwOnCancel is false. onError never fires for them. Always safe to call — with nothing in flight it returns 0.


pending(selector?)

pending(selector?: CancelSelector): PendingRequest[];

The cancelable requests currently in flight, optionally filtered by the same selectors cancel() accepts.

if (api.pending("/api/v1/products").length) showSpinner();

cancelScope(name?)

cancelScope(name?: string): CancelScope;

Creates a cancellation scope — a wrapper whose requests are all tagged, so one call stops the lot.

const scope = api.cancelScope("product-modal");
await scope.get("/api/v1/products/12");
scope.cancel();          // everything the scope started
scope.pending();         // just the scope's requests

Requests made through a scope are cancelable by default, whatever the client-wide setting — creating the scope is the opt-in. Writes still need cancelable: true. An omitted name gets a unique generated one.

See [[Cancellation]].


isWorker

readonly isWorker: boolean;

Whether requests actually run in a Web Worker. Useful for asserting your security posture in production.


destroy()

destroy(): void;

Terminates the worker, aborts pending requests (their promises reject with "Client destroyed"), closes the tab channel, clears listeners and revokes the blob URL.

Call it for short-lived clients — per-request server clients, tests, torn-down micro-frontends.

const api = createClient({ baseUrl, worker: false });
try {
  return (await api.get("/users")).data;
} finally {
  api.destroy();
}

ApiError

class ApiError extends Error {
  readonly name: "ApiError";
  readonly statusCode: number;
  readonly errors?: Record<string, string[]>;
  readonly data?: unknown;
  readonly response: IRes<unknown>;
  readonly canceled: boolean;
  readonly cancelReason?: string;
  constructor(response: IRes<unknown>);
}
try {
  await api.post("/users", input);
} catch (e) {
  if (e instanceof ApiError && e.statusCode === 422) showFieldErrors(e.errors);
  else throw e;
}

buildQueryString(params, prefix?)

function buildQueryString(params: Record<string, unknown>, prefix?: string): string;

The client's serializer, exported for standalone use. Nested objects become brackets, arrays repeat the key, Dates become ISO strings, and null/undefined/"" are dropped at every depth.

buildQueryString({ a: 1, b: { c: [1, 2] } });
// "a=1&b%5Bc%5D=1&b%5Bc%5D=2"

buildQueryString({ page: 1, q: "", tags: ["x", null, "y"] });
// "page=1&tags=x&tags=y"

getTokenExpiry(token?)

function getTokenExpiry(token?: string | null): number | null;

Reads the JWT exp claim as epoch milliseconds. Returns null for anything that isn't a three-part JWT with a numeric exp. Never verifies the signature.

const exp = getTokenExpiry(token);
if (exp) console.log("expires", new Date(exp).toLocaleString());

isTokenExpired(token, skewMs?)

function isTokenExpired(token: string | null | undefined, skewMs?: number): boolean;

true when the token has a known expiry that has already passed (optionally shifted by skewMs). Returns false when the expiry is unknown — "unknown" is not "expired".

isTokenExpired(token);          // is it dead now?
isTokenExpired(token, 60_000);  // will it be dead in a minute?

Storage classes

class MemoryStorage implements TokenStorage {
  constructor();
}

class WebStorage implements TokenStorage {
  constructor(key: string, kind: "local" | "session");
}

class CookieStorage implements TokenStorage {
  constructor(key: string, days?: number); // days defaults to 7
}

All three implement get() / set(tokens) / clear() and swallow storage errors (Safari private mode, quota, corrupt JSON) rather than throwing. See [[Storage Adapters]].


Quick index

ExportKindPurpose
createClientfunctionCreate a client
ApiErrorclassThrown on failure
buildQueryStringfunctionSerialize params
getTokenExpiryfunctionRead a JWT exp
isTokenExpiredfunctionExpiry check with skew
MemoryStorageclassIn-memory adapter
WebStorageclasslocalStorage/sessionStorage adapter
CookieStorageclassNon-httpOnly cookie adapter
ApiClienttypeThe client interface
ClientOptionstypecreateClient options
RequestConfig<T>typePer-request options
CancelSelectortypeWhat cancel() accepts
CancelMatchtypeThe object selector form
CancelOptionstypeThe cancel client option
CancelScopetypeA scope returned by cancelScope()
PendingRequesttypeOne tracked in-flight request
IRes<R>typeThe response envelope
AuthStatetypeAuth state (never tokens)
TokenPairtype{ accessToken?, refreshToken?, expiresAt? }
TokenStoragetypeCustom adapter interface
TokenExtractortype(body) => TokenPair | null
LogEntrytypeStructured log record
Params<T>typeQuery params, with typed ordering
ListResponse<T>type{ count, next, previous, results }
Ordering<T>type{ [K in keyof T]?: "asc" | "desc" }
HttpMethodtype"GET" | "POST" | …
AuthModetype"header" | "cookie"
StorageKindtype"memory" | "local" | "session" | "cookie"

Next: [[TypeScript Types]]

On this page