Skip to content

Latest commit

 

History

History
637 lines (489 loc) · 31.7 KB

DOCS.md

File metadata and controls

637 lines (489 loc) · 31.7 KB

fetchify docs

Reference for fetchify's public API. For install/run/release instructions, see README.md.

This file is kept in sync with the package's exports — any change that adds, modifies, or removes a public export should update the relevant section here.

Entry points

fetchify exposes a main entry point, plus opt-in subpaths for pieces you don't always need:

  • @kamsteegsoftware/fetchify — the package's API: createFetchify, defineOptions, defineHeaders, defineBeforeRequestHook, createCacheAdapter, FetchifyError, FetchifyTimeoutError, and their associated types (dist/index.js)
  • @kamsteegsoftware/fetchify/cache/memory — the built-in in-memory CacheAdapter implementation (dist/cache/memory.js), kept out of the main entry so consumers who don't use it don't bundle it
  • @kamsteegsoftware/fetchify/cache/redis — the built-in Redis-backed CacheAdapter implementation (dist/cache/redis.js), for Bun.RedisClient or ioredis clients
  • @kamsteegsoftware/fetchify/cache/upstash — the built-in Upstash REST-backed CacheAdapter implementation (dist/cache/upstash.js), for @upstash/redis clients
  • @kamsteegsoftware/fetchify/logger/console — the built-in console LoggerAdapter implementation (dist/logger/console.js), kept out of the main entry so consumers who don't use it don't bundle it
import { createFetchify, defineOptions, defineHeaders } from "@kamsteegsoftware/fetchify";

createFetchify

function createFetchify(options?: FetchifyOptions): Fetchify;

Creates an HTTP client. Accepts an optional FetchifyOptions object (or the result of defineOptions) and returns a Fetchify client object with one lowercase method per supported HTTP verb:

interface Fetchify {
  get: FetchifyMethod;
  post: FetchifyMethod;
  put: FetchifyMethod;
  patch: FetchifyMethod;
  delete: FetchifyMethod;
  head: FetchifyMethod;
  options: FetchifyMethod;
}

Each method has the FetchifyMethod signature: it takes a path and an optional FetchifyMethodOptions, plus an optional type parameter T, and returns a Promise<FetchifyResponse<T>>.

If options.baseUrl is set, request paths are resolved against it; otherwise the path is passed straight through to fetch.

const client = createFetchify({ baseUrl: "https://api.example.com" });

try {
  const { data, response } = await client.get<User>("/users/1");
  // data: User | null — JSON-parsed body, or null if the body couldn't be parsed
  // response: Response — unread clone of the raw fetch Response
} catch (error) {
  if (error instanceof FetchifyError) {
    console.error(error.status, error.data); // non-ok response
  }
  throw error;
}

const created = await client.post("/users", {
  body: JSON.stringify({ name: "Ada" }),
});

Calling createFetchify() or createFetchify({}) returns a client with the same methods and no base URL resolution applied.

A non-ok (response.ok === false) response never resolves — the call rejects with a FetchifyError instead. Network failures (a rejected fetch() call) propagate unchanged, so a single try/catch covers both.

options.cache accepts a CacheAdapter (see createCacheAdapter). When set, every GET request is looked up in the cache first, keyed by `${method} ${url}` where url is the resolved, absolute request URL (after baseUrl and params are applied). A cache hit resolves with the stored data/response and cached: true, without calling fetch. On a miss, the request goes to the network as usual; if the response is ok, it's written back to the cache — with no explicit ttlMs, leaving expiry to the adapter's own default (see createCacheAdapter's cacheTtlMs) — before resolving with cached: false. GET responses that aren't ok never write to the cache. If a cache adapter call throws, the request falls back to the network rather than failing.

Cache invalidation is opt-in and explicit, per request — there's no automatic invalidation of any kind. Every FetchifyMethod call, regardless of verb, accepts an invalidate?: boolean | (string | URL)[] option, actioned when the request's response resolves as a live (non-cache-hit) ok response:

  • invalidate: true deletes the GET cache entry for that request's own URL — the same one a matching GET would use — without repeating the path yourself:

    await client.put("/users/1", {
      body: { name: "Ada" },
      invalidate: true,
    });
  • An array of entries resolves each one to a URL — a string entry the same way a request path is resolved (joined against baseUrl when configured, used as-is otherwise), a URL entry used as-is — and deletes the GET cache entry for each:

    await client.put("/users/1", {
      body: { name: "Ada" },
      invalidate: ["/users/1", "/users"],
    });

Omitting invalidate (or passing false/[]) leaves a request's cache behavior completely unchanged — no adapter method is called beyond the pre-existing GET read/write behavior. invalidate never runs when the response isn't ok, or when a GET request is itself served from the cache. Each entry (the own-URL entry from true, or each array entry) is deleted independently: a delete failure for one entry is swallowed and doesn't block the rest, and never affects the resolved response.

options.headers accepts a Record<string, string | undefined> (see defineHeaders) of default headers sent with every request made by the client:

const client = createFetchify({
  baseUrl: "https://api.example.com",
  headers: { Authorization: `Bearer ${token}` },
});

options.logger accepts a LoggerAdapter. When set, every request calls logger.log(entry) exactly once after it settles (resolves or throws), with a FetchifyLogEntry describing the request's method, resolved url, status (null if the request threw before a response was received), durationMs, and cached. The call is fire-and-forget: it isn't awaited, and any error the logger throws or rejects with is caught and discarded, so a misbehaving logger never affects the resolved value or thrown error of the request it's logging. With no logger configured, no logging occurs — see the built-in @kamsteegsoftware/fetchify/logger/console adapter for a ready-to-use default.

options.hooks?.beforeRequest accepts a BeforeRequestHook (see defineBeforeRequestHook), invoked with the fully-resolved request immediately before every outgoing fetch call (client defaults and per-call options already merged, params already applied to the URL). It's skipped entirely on a cache hit, since no fetch call happens there. The hook may resolve to a partial override — url, headers, body, and/or init — applied on top of the resolved request using the same merge rules as FetchifyMethodOptions (headers merge with undefined-removal, body replaces the resolved body, init is spread alongside the computed values); resolving to undefined leaves the request unchanged. If the hook throws or its promise rejects, the error propagates to the caller and no request is sent:

const client = createFetchify({
  baseUrl: "https://api.example.com",
  hooks: {
    beforeRequest: async (ctx) => ({
      headers: { Authorization: `Bearer ${await getToken()}` },
    }),
  },
});

options.timeout sets the default number of milliseconds a request may take before it's aborted, applied to every request made by the client. A single call can override it via options.timeout on that FetchifyMethod call — including 0 to explicitly disable an inherited client-level timeout for that call. The timeout only bounds the underlying fetch call itself; time spent in beforeRequest or reading from a cache adapter doesn't count against it, and a cache hit is never subject to a timeout (no fetch call happens there). When the timeout elapses first, the call rejects with a FetchifyTimeoutError instead of resolving. A caller-supplied options.init.signal still works as before and composes with the timeout — whichever fires first determines the request's outcome, and a caller-triggered abort is never reported as a FetchifyTimeoutError. With no timeout configured anywhere, requests behave exactly as they did before this option existed, waiting on fetch indefinitely.

const client = createFetchify({ baseUrl: "https://api.example.com", timeout: 5000 });

try {
  await client.get("/slow");
} catch (error) {
  if (error instanceof FetchifyTimeoutError) {
    console.error(`timed out after ${error.timeoutMs}ms`);
  }
  throw error;
}

// Override the client default for a single call
await client.get("/slow", { timeout: 500 });

options.retry accepts a FetchifyRetryOptions object, applied as the default retry configuration for every request made by the client; omitting it makes every request exactly one network attempt, unchanged from before this option existed. A single call can override it via retry on that FetchifyMethod call — either a different FetchifyRetryOptions object (replacing the client default outright, not merging with it) or false to disable retry for that call. With no retryOn supplied, only idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE) are retried, and only for network failures, a FetchifyTimeoutError, or a FetchifyError with status 408, 429, 500, 502, 503, or 504 — POST/PATCH are never retried by default. Each retry waits using capped exponential backoff with full jitter (or the response's Retry-After header, for a 429/503), re-runs beforeRequest, and gets its own fresh timeout window. A caller-triggered AbortSignal is never retried. Retry never applies to a cache-served GET, since no fetch call is made for a cache hit.

const client = createFetchify({
  baseUrl: "https://api.example.com",
  retry: { retries: 3 }, // exponential backoff with jitter, 200ms base delay
});

await client.get("/flaky"); // retried on network errors, timeouts, and 408/429/5xx

// Override the client default for a single call
await client.get("/flaky", { retry: { retries: 1, retryOn: () => true } });
await client.post("/orders", { body, retry: false }); // never retried

defineOptions

function defineOptions(options?: FetchifyOptions): FetchifyOptions;

Identity helper that returns its argument unchanged, giving consumers a typed entry point for authoring FetchifyOptions independently of createFetchify. The object it returns can be passed directly to createFetchify.

const options = defineOptions({ baseUrl: "https://api.example.com" });
const client = createFetchify(options);

defineHeaders

function defineHeaders(headers?: Record<string, string | undefined>): Record<string, string>;

Builds a headers object, omitting any entry whose value is undefined — useful for conditionally-set headers (e.g. an optional auth token) without manually filtering them out:

const headers = defineHeaders({
  Authorization: token ? `Bearer ${token}` : undefined,
});
// no Authorization key at all when token is falsy, instead of "undefined"

The result can be passed as createFetchify's headers option or as a FetchifyMethod call's options.headers.

defineBeforeRequestHook

function defineBeforeRequestHook(hook: BeforeRequestHook): BeforeRequestHook;

Identity helper, analogous to defineOptions, that types a consumer-supplied function against the BeforeRequestHook signature and returns it unchanged — a typed, discoverable entry point for authoring a beforeRequest hook independently of createFetchify:

import { createFetchify, defineOptions, defineBeforeRequestHook } from "@kamsteegsoftware/fetchify";

const beforeRequest = defineBeforeRequestHook(async (ctx) => ({
  headers: { Authorization: `Bearer ${await getToken()}` },
}));

const client = createFetchify(defineOptions({ hooks: { beforeRequest } }));

createCacheAdapter

function createCacheAdapter(
  adapter: CacheAdapter,
  options?: { cacheTtlMs?: number },
): CacheAdapter;

Identity helper, analogous to defineOptions, that types a consumer-supplied object against the CacheAdapter interface and returns it (see below). This is the supported pattern for building your own cache adapter — for AsyncStorage, a database, or any backend fetchify doesn't ship a built-in for (see @kamsteegsoftware/fetchify/cache/redis and @kamsteegsoftware/fetchify/cache/upstash for Redis and Upstash):

import { createCacheAdapter } from "@kamsteegsoftware/fetchify";

const adapter = createCacheAdapter({
  async get(key) {
    /* ... */
  },
  async set(key, value, options) {
    /* ... */
  },
  async delete(key) {
    /* ... */
  },
  async has(key) {
    /* ... */
  },
});

options.cacheTtlMs, when provided, becomes the returned adapter's default ttlMs for any set call that omits its own; an explicit ttlMs passed to a given set call always takes precedence over cacheTtlMs. Omitting options, or cacheTtlMs, returns the given adapter unchanged:

const adapter = createCacheAdapter(myAdapter, { cacheTtlMs: 60_000 });

await adapter.set("key", "value"); // myAdapter.set("key", "value", { ttlMs: 60_000 })
await adapter.set("key", "value", { ttlMs: 1_000 }); // { ttlMs: 1_000 }

Built-in: @kamsteegsoftware/fetchify/cache/memory

function createMemoryCacheAdapter(options?: { ttlMs?: number }): CacheAdapter; // default export

An in-memory CacheAdapter, built with createCacheAdapter, with no external dependencies — works on both web and React Native. Each call returns a fresh, independent adapter backed by a Map, with lazy TTL expiry (an expired entry is dropped the next time it's read). options.ttlMs sets the adapter's default ttlMs, per createCacheAdapter's cacheTtlMs.

import createMemoryCacheAdapter from "@kamsteegsoftware/fetchify/cache/memory";

const cache = createMemoryCacheAdapter();
await cache.set("key", "value", { ttlMs: 60_000 });

Built-in: @kamsteegsoftware/fetchify/cache/redis

interface RedisLike {
  get(key: string): Promise<string | null | undefined>;
  set(
    key: string,
    value: string,
    px?: "PX",
    milliseconds?: number,
  ): Promise<unknown>;
  del(...keys: string[]): Promise<number>;
  exists(key: string): Promise<boolean | number>;
}

function createRedisCacheAdapter(options: {
  client: RedisLike;
  ttlMs?: number;
}): CacheAdapter; // default export

A Redis-backed CacheAdapter, built with createCacheAdapter. options.client is an already-connected client conforming to the minimal RedisLike interface — both Bun.RedisClient (Bun.redis / new Bun.RedisClient(...)) and an ioredis client satisfy it as-is, so neither package is a dependency of fetchify; bring whichever client you already use. get/set/delete/has map onto Redis' GET/SET .. PX/DEL/EXISTS. options.ttlMs, per createCacheAdapter's cacheTtlMs, becomes the adapter's default expiry, enforced by Redis itself via SET key value PX ttlMs.

import createRedisCacheAdapter from "@kamsteegsoftware/fetchify/cache/redis";

const cache = createRedisCacheAdapter({ client: Bun.redis, ttlMs: 60_000 });
const client = createFetchify({ cache });

Built-in: @kamsteegsoftware/fetchify/cache/upstash

interface UpstashLike {
  get(key: string): Promise<string | null | undefined>;
  set(key: string, value: string, opts?: { px?: number }): Promise<unknown>;
  del(...keys: string[]): Promise<number>;
  exists(...keys: string[]): Promise<number>;
}

function createUpstashCacheAdapter(options: {
  client: UpstashLike;
  ttlMs?: number;
}): CacheAdapter; // default export

An Upstash REST-backed CacheAdapter, built with createCacheAdapter, for runtimes without raw TCP socket access (edge functions, Cloudflare Workers, other serverless environments). options.client is an already-constructed client conforming to the minimal UpstashLike interface — an @upstash/redis Redis instance satisfies it as-is, so @upstash/redis is not a dependency of fetchify; bring your own instance. get/set/delete/has map onto the client's get/set/del/exists. options.ttlMs, per createCacheAdapter's cacheTtlMs, becomes the adapter's default expiry, passed as set's px option.

import { Redis } from "@upstash/redis";
import createUpstashCacheAdapter from "@kamsteegsoftware/fetchify/cache/upstash";

const cache = createUpstashCacheAdapter({
  client: new Redis({
    url: process.env.UPSTASH_REDIS_REST_URL!,
    token: process.env.UPSTASH_REDIS_REST_TOKEN!,
  }),
  ttlMs: 60_000,
});
const client = createFetchify({ cache });

Built-in: @kamsteegsoftware/fetchify/logger/console

function createConsoleLogger(): LoggerAdapter; // default export

A simple built-in LoggerAdapter, with no external dependencies — works on both web and React Native. Each call returns a fresh, independent adapter that prints one line per FetchifyLogEntry to console: a successful entry goes to console.info as [fetchify] <METHOD> <url> <status> (<durationMs>ms), with a trailing [cached] when cached is true; an entry with a null status (the request threw before a response was received) goes to console.error as [fetchify] <METHOD> <url> FAILED (<durationMs>ms). durationMs is rounded to the nearest integer for display. There's no configurable format, log level, or filtering — for anything beyond this simple default, write your own LoggerAdapter.

import createConsoleLogger from "@kamsteegsoftware/fetchify/logger/console";

const client = createFetchify({ logger: createConsoleLogger() });

Types

FetchifyOptions

interface FetchifyOptions {
  baseUrl?: string | URL;
  cache?: CacheAdapter;
  logger?: LoggerAdapter;
  headers?: Record<string, string | undefined>;
  hooks?: FetchifyHooks;
  timeout?: number;
  retry?: FetchifyRetryOptions;
}

Configuration accepted by createFetchify and defineOptions. headers (see defineHeaders) are the default headers sent with every request made by the client. hooks (see FetchifyHooks) configures lifecycle hooks, currently just beforeRequest. timeout is the default number of milliseconds a request may take before it's aborted with a FetchifyTimeoutError; see createFetchify for the full behavior. retry is the default FetchifyRetryOptions applied to every request; see createFetchify for the full behavior.

FetchifyRetryOptions

interface FetchifyRetryOptions {
  retries?: number;
  delay?: number;
  backoffFactor?: number;
  maxDelay?: number;
  jitter?: boolean;
  retryOn?: (context: FetchifyRetryContext) => boolean;
}

Retry configuration accepted by FetchifyOptions.retry (client-level default) and FetchifyMethodOptions.retry (per-call override). Every field is optional; supplying {} enables retry with every default applied.

  • retries — maximum number of retry attempts after the initial one. Default 3.
  • delay — base delay, in milliseconds, before the first retry. Default 200.
  • backoffFactor — multiplier applied to the delay on each subsequent retry. Default 2.
  • maxDelay — upper bound, in milliseconds, on any computed delay (including a Retry-After-derived one). Default 30000.
  • jitter — when true, the actual wait is a random value between 0 and the computed delay ("full jitter"), instead of the computed delay exactly. Default true.
  • retryOn — overrides the default eligibility predicate entirely, for every method including POST/PATCH. Called with a FetchifyRetryContext; returning true retries, false stops and propagates the failure. Never consulted for a caller-triggered AbortSignal, which is never retried.

FetchifyRetryContext

interface FetchifyRetryContext {
  method: string;
  url: string;
  attempt: number;
  error: FetchifyError | FetchifyTimeoutError | Error;
}

Passed to a custom FetchifyRetryOptions.retryOn after each failed attempt: the uppercase HTTP verb, the fully resolved request URL, the 1-based count of attempts made so far (including the one that just failed), and the error that triggered the eligibility check.

FetchifyHooks

interface FetchifyHooks {
  beforeRequest?: BeforeRequestHook;
}

Container for createFetchify's lifecycle hooks, grouped under FetchifyOptions.hooks so future hooks (e.g. an afterResponse) have a home without widening FetchifyOptions' top-level shape.

BeforeRequestHook

type BeforeRequestHook = (
  context: BeforeRequestContext,
) => Promise<BeforeRequestOverrides | void>;

Signature of the beforeRequest hook (see createFetchify and defineBeforeRequestHook). Always awaited, whether or not the given function is itself declared async.

BeforeRequestContext

interface BeforeRequestContext {
  method: string;
  url: URL | string;
  headers: Record<string, string>;
  body: RequestInit["body"];
  init?: RequestInit;
}

The fully-resolved request passed to beforeRequest: method is the uppercase HTTP verb, url and headers reflect client defaults merged with per-call options (with params already applied to url), and body is the already-serialized request body.

BeforeRequestOverrides

interface BeforeRequestOverrides {
  url?: string | URL;
  headers?: Record<string, string | undefined>;
  body?: RequestInit["body"] | JsonSerializable;
  init?: RequestInit;
}

Partial override a beforeRequest hook may resolve to. Each present field is applied on top of the already-resolved request before fetch is called: headers merges over the resolved headers (undefined removes a key); body and url, if present, replace the resolved value outright; init is spread the same way FetchifyMethodOptions.init already is, with the computed method/headers/body still taking precedence. Fields left out are untouched.

CacheAdapter

interface CacheAdapter {
  get(key: string): Promise<string | undefined>;
  set(
    key: string,
    value: string,
    options?: { ttlMs?: number },
  ): Promise<void>;
  delete(key: string): Promise<void>;
  has(key: string): Promise<boolean>;
}

A storage- and platform-agnostic async key/value store. Build one with createCacheAdapter, or use the built-in @kamsteegsoftware/fetchify/cache/memory adapter.

LoggerAdapter

interface LoggerAdapter {
  log(entry: FetchifyLogEntry): void | Promise<void>;
}

A structural interface for observing requests made by a Fetchify client — any object with a compatible log method satisfies it, with no base class or fetchify dependency required. Pass one as createFetchify's logger option, write your own (an async log may return a Promise, which fetchify does not await), or use the built-in @kamsteegsoftware/fetchify/logger/console adapter:

const logger: LoggerAdapter = {
  log(entry) {
    myPinoLogger.info(entry);
  },
};

FetchifyLogEntry

interface FetchifyLogEntry {
  method: string;
  url: string;
  status: number | null;
  durationMs: number;
  cached: boolean;
}

The value passed to LoggerAdapter.log for each request: the uppercase HTTP verb, the fully resolved request URL, the response's numeric HTTP status (null if the request threw before a response was received), the elapsed time in milliseconds, and whether the response was served from a configured cache adapter.

FetchifyMethod

type FetchifyMethod = <T = string>(
  path: string,
  options?: FetchifyMethodOptions,
) => Promise<FetchifyResponse<T>>;

Signature of each HTTP verb method on a Fetchify client. The type parameter T types the resolved FetchifyResponse's data field and defaults to string.

FetchifyMethodOptions

interface FetchifyMethodOptions {
  headers?: Record<string, string | undefined>;
  params?: Record<string, string | number | boolean | undefined>;
  body?: RequestInit['body'] | JsonSerializable;
  init?: RequestInit;
  timeout?: number;
  retry?: FetchifyRetryOptions | false;
  invalidate?: boolean | (string | URL)[];
}

Per-request options accepted by every FetchifyMethod call:

  • headers — merged over the client's default headers (see createFetchify) for this request only. A string value sets/overrides a header; undefined removes it, even if the client default set a value for it.
  • params — query parameters merged into the request URL's query string. A string, number, or boolean value sets/overrides that query parameter (number/boolean values are converted with String(value)), including one already present in the path; undefined removes it.
  • body — the request body. Accepts either a standard BodyInit value (string, Blob, ArrayBuffer/typed array, FormData, URLSearchParams, ReadableStream) or any JSON-serializable value (plain object, array, number, boolean, null). When the resolved Content-Type header (see headers above) is application/json, a non-BodyInit body is automatically run through JSON.stringify before being sent; otherwise it's forwarded to fetch exactly as given, with no auto-serialization and no auto-setting of Content-Type. Standard BodyInit values are never re-serialized, even when Content-Type is application/json, so an already-JSON.stringify'd string is never double-encoded. Takes precedence over init.body when both are set — init.body is never serialized, regardless of Content-Type.
  • init — the underlying RequestInit, for request settings not covered by headers/params/body (e.g. signal, credentials). Its headers and body fields, if set, are superseded by the computed headers and body above rather than merged with them.
  • timeout — overrides the client's default timeout (see createFetchify) for this request only; 0 explicitly disables an inherited client-level timeout.
  • retry — overrides the client's default retry (see createFetchify) for this request only, replacing it outright rather than merging; false explicitly disables an inherited client-level retry.
  • invalidate — purges cache entries after this request resolves as a live ok response (see options.cache above). true purges this request's own URL; an array purges each listed entry, resolved the same way a request path does. Omitted, false, or [] means no cache interaction beyond this request's own read/write behavior.
await client.get("/users", {
  headers: { Authorization: `Bearer ${token}` },
  params: { active: "true" },
});

// Auto-serialized to JSON, since Content-Type is application/json
await client.post("/users", {
  headers: { "Content-Type": "application/json" },
  body: { name: "Ada" },
});

// No Content-Type set — forwarded to fetch unchanged, so pre-stringify it yourself
await client.post("/users", {
  body: JSON.stringify({ name: "Ada" }),
});

JsonSerializable

type JsonSerializable =
  Record<string, unknown> |
  unknown[] |
  string |
  number |
  boolean |
  null;

The set of value shapes FetchifyMethodOptions.body accepts beyond the standard BodyInit types — used to trigger the Content-Type-driven JSON.stringify behavior described above.

FetchifyResponse<T>

interface FetchifyResponse<T = string> {
  data: T | null;
  response: Response;
  cached: boolean;
}

Resolved value of every Fetchify client method. Only ever resolves for an ok response — a non-ok response rejects with a FetchifyError instead (see below).

  • data — the response body. The body is read as text and JSON.parsed: the parsed value is used if that succeeds, otherwise the raw text is used. null only if the body couldn't be read.
  • response — a clone of the original fetch Response, taken before data is read, so its body is still unread. Use it for .json(), .blob(), status, headers, ok, etc.
  • cached — true when data/response were served from a configured cache adapter instead of the network (see createFetchify); false otherwise, including whenever no cache adapter is configured.
try {
  const { data, cached } = await client.get<User>("/users/1");
  console.log(data); // User | null
} catch (error) {
  if (error instanceof FetchifyError) {
    console.error(error.status);
  }
}

FetchifyError<T>

class FetchifyError<T = string> extends Error {
  status: number;
  statusText: string;
  response: Response;
  data: T | null;
  request: { method: string; url: string };
}

Thrown by every Fetchify client method when the underlying response is non-ok (response.ok === false), instead of resolving a FetchifyResponse.

  • status / statusText — the response's HTTP status code and status text.
  • response — an unread clone of the fetch Response, usable for .json(), .text(), .blob(), headers, etc.
  • data — the error body, parsed with the same Content-Type-driven rule as FetchifyResponse['data'] (JSON-parsed when Content-Type is application/json, null on parse failure; raw text otherwise). Carries the same type parameter T passed to the method call.
  • request — { method, url } of the request that failed, with url fully resolved (baseUrl and params applied).
try {
  await client.get("/users/1");
} catch (error) {
  if (error instanceof FetchifyError) {
    console.error(error.request.method, error.request.url, error.status, error.data);
  } else {
    throw error; // network failure or something else entirely
  }
}

A cache hit is never non-ok (cache entries are only ever written from ok responses), so a cache-served GET never throws FetchifyError.

FetchifyTimeoutError

class FetchifyTimeoutError extends Error {
  request: { method: string; url: string };
  timeoutMs: number;
}

Thrown by every Fetchify client method when a configured timeout (see createFetchify) elapses before the underlying fetch call settles. Distinct from FetchifyError — since no HTTP response was ever received, there's no response, status, or data to expose.

  • request — { method, url } of the request that timed out, with url fully resolved.
  • timeoutMs — the timeout duration, in milliseconds, that elapsed.
try {
  await client.get("/slow", { timeout: 500 });
} catch (error) {
  if (error instanceof FetchifyTimeoutError) {
    console.error(error.request.method, error.request.url, error.timeoutMs);
  } else {
    throw error;
  }
}

A cache hit is never subject to a timeout (no fetch call happens there), so a cache-served GET never throws FetchifyTimeoutError.

VERSION

const VERSION: string;

The package version, currently "0.0.0".