Skip to content

Latest commit

 

History

History
255 lines (184 loc) · 9.3 KB

File metadata and controls

255 lines (184 loc) · 9.3 KB

Getting started with fetchify

A walkthrough for installing fetchify, making your first request, and adding caching. For the full API reference, see DOCS.md.

Installation

Add fetchify as a dependency with bun install:

bun install @kamsteegsoftware/fetchify

Basic usage

Create a client with createFetchify. If you pass baseUrl, request paths are resolved against it; otherwise the path is passed straight through to fetch.

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

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

Each HTTP verb (get, post, put, patch, delete, head, options) is a method on the client. It takes a path and an optional RequestInit, and resolves a FetchifyResponse — or rejects with a FetchifyError if the response isn't ok:

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

interface User {
  id: string;
  name: string;
}

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 — an unread clone of the raw fetch Response
  console.log(data);
} catch (error) {
  if (error instanceof FetchifyError) {
    console.error(error.status, error.data); // non-ok response, e.g. 404
  } else {
    throw error; // network failure or something else entirely
  }
}

A non-ok (response.ok === false) response never resolves — it rejects with a FetchifyError carrying status, statusText, response, data (the error body, parsed the same way as a success response), and request. See DOCS.md for the full shape.

Mutating requests work the same way — pass a body. If the request's Content-Type header is application/json (set on the client via headers, as below, or per-request), a plain object body is automatically serialized with JSON.stringify:

const client = createFetchify({
  baseUrl: "https://api.example.com",
  headers: { "Content-Type": "application/json" },
});

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

Without a Content-Type: application/json header, body is forwarded to fetch exactly as given — pass an already-serialized string yourself in that case:

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

FetchifyResponse also carries a cached flag, which is true when the response came from a cache adapter instead of the network (see below):

const first = await client.get<User>("/users/1");
console.log(first.cached); // false — served over the network

const second = await client.get<User>("/users/1");
console.log(second.cached); // true, once a cache adapter is configured

Adding a beforeRequest hook

Pass a hooks.beforeRequest function to run right before every outgoing request — handy for attaching a freshly-fetched auth token:

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

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

The hook receives the fully-resolved request (method, url, headers, body) and can return a partial override — only the fields you return are changed, everything else is left as already resolved. It doesn't run on a cache hit, since no request goes out in that case. See DOCS.md for the full merge rules.

Setting a request timeout

Pass timeout (in milliseconds) to bound how long a request may take before it's aborted:

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

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's default for a single call, including disabling it with 0:

await client.get("/slow", { timeout: 500 }); // shorter deadline for this call
await client.get("/slow", { timeout: 0 }); // no timeout for this call

With no timeout configured, requests wait on the network indefinitely, same as before this option existed. See DOCS.md for how it composes with a caller-supplied AbortSignal.

Retrying failed requests

Pass retry to automatically retry a request on a transient failure — a network error, a timeout, or a 408/429/5xx response — using exponential backoff with jitter:

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

await client.get("/flaky"); // retried up to 3 times before rejecting

By default only idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE) are retried, so a POST/PATCH never gets retried automatically — pass your own retryOn if you know a specific write is safe to retry (e.g. it's protected by an idempotency key):

await client.post("/orders", {
  body: { id: "already-generated-client-side" },
  retry: { retries: 2, retryOn: () => true },
});

Override the client's default for a single call, including disabling it outright:

await client.get("/flaky", { retry: { retries: 1 } }); // replaces the client default
await client.get("/flaky", { retry: false }); // never retried for this call

With no retry configured, requests behave exactly as before this option existed — one attempt, no retry. See DOCS.md for the full default eligibility rules, backoff/jitter formula, and Retry-After handling.

Using cache adapters

Pass a CacheAdapter as the cache option to createFetchify to cache GET responses. Caching only applies to GET requests — other verbs always hit the network. On a GET, fetchify checks the cache first; on a cache miss, it makes the request and, if the response is ok, stores it for next time.

Built-in in-memory adapter

@kamsteegsoftware/fetchify/cache/memory ships an in-memory adapter with no external dependencies, backed by a Map with lazy TTL expiry:

import { createFetchify } from "@kamsteegsoftware/fetchify";
import createMemoryCacheAdapter from "@kamsteegsoftware/fetchify/cache/memory";

const cache = createMemoryCacheAdapter();

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

const { data, cached } = await client.get<User>("/users/1");

Each call to createMemoryCacheAdapter() returns a fresh, independent store — it doesn't persist across process restarts, and is best suited to short-lived caching within a single running process.

Built-in Redis adapter

For a shared, cross-process cache, @kamsteegsoftware/fetchify/cache/redis wraps an already-connected Redis client — either Bun.RedisClient (Bun.redis) or an ioredis instance both work, without adding either package as a dependency of fetchify:

import { createFetchify } from "@kamsteegsoftware/fetchify";
import createRedisCacheAdapter from "@kamsteegsoftware/fetchify/cache/redis";

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

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

ttlMs is optional and sets the default expiry (in milliseconds) for cache entries that don't specify their own; Redis enforces it natively via SET .. PX.

Built-in Upstash adapter

For runtimes without raw TCP socket access — edge functions, Cloudflare Workers, and similar serverless environments — @kamsteegsoftware/fetchify/cache/upstash wraps an @upstash/redis Redis client (Upstash's REST-based SDK), again without fetchify depending on the package itself:

import { Redis } from "@upstash/redis";
import { createFetchify } from "@kamsteegsoftware/fetchify";
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({
  baseUrl: "https://api.example.com",
  cache,
});

Custom cache adapters

For AsyncStorage, a database, or any other backend fetchify doesn't ship a built-in for, build your own with createCacheAdapter, which types a plain object against the CacheAdapter interface and returns it unchanged:

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

const adapter = createCacheAdapter({
  async get(key) {
    /* return the cached value for key, or undefined */
  },
  async set(key, value, options) {
    /* store value under key, honoring options?.ttlMs if set */
  },
  async delete(key) {
    /* remove key from the cache */
  },
  async has(key) {
    /* return whether key exists and hasn't expired */
  },
});

const client = createFetchify({ cache: adapter });

For the full CacheAdapter interface and createCacheAdapter's cacheTtlMs option, see DOCS.md.

Next steps

See DOCS.md for the complete API reference, including defineOptions and full type signatures.