A walkthrough for installing fetchify, making your first request, and adding caching. For the full API reference, see DOCS.md.
Add fetchify as a dependency with bun install:
bun install @kamsteegsoftware/fetchifyCreate 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 configuredPass 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.
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 callWith 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.
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 rejectingBy 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 callWith 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.
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.
@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.
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.
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,
});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.
See DOCS.md for the complete API reference, including defineOptions and full type signatures.