An advanced fetch library that actually solves real problems.
Documentation · Getting Started · Plugins
Fetch is too basic for real apps. You end up writing the same boilerplate: error handling, retries, deduplication, response parsing etc. CallApi handles all of that and practically more.
Drop-in replacement for fetch. Under 6KB. Zero dependencies.
import { callApi } from "@zayne-labs/callapi";
const { data, error } = await callApi("/api/users");
Request Deduplication - User spam-clicks a button? Handled. No race conditions.
const req1 = callApi("/api/user");
const req2 = callApi("/api/user"); // Shares req1's response
Smart Response Parsing - Looks at Content-Type, does the right thing.
const { data } = await callApi("/api/data"); // JSON? Parsed.
Error Handling - Structured errors you can actually use.
const { data, error } = await callApi("/api/users");
if (error) {
console.log(error.name); // "HTTPError", "ValidationError"
console.log(error.errorData); // Actual API response
}
Retries - Exponential backoff, custom conditions.
await callApi("/api/data", {
retryAttempts: 3,
retryStrategy: "exponential",
retryStatusCodes: [429, 500, 502, 503],
});
Schema Validation - TypeScript types + runtime validation.
import { z } from "zod";
import { defineSchema, createFetchClient } from "@zayne-labs/callapi";
const api = createFetchClient({
schema: defineSchema({
"/users/:id": {
data: z.object({
id: z.number(),
name: z.string(),
}),
},
}),
});
const user = await api("/users/123"); // Fully typed + validated
Hooks - Intercept at any point.
const api = createFetchClient({
onRequest: ({ request }) => {
request.headers.set("Authorization", `Bearer ${token}`);
},
onError: ({ error }) => {
Sentry.captureException(error);
},
onResponseStream: ({ event }) => {
console.log(`Downloaded ${event.progress}%`);
},
});
Plugins - Extend with middleware.
const cachingPlugin = definePlugin({
id: "caching-plugin",
name: "Caching plugin",
middlewares: (ctx) => {
const cache = new Map();
return {
fetchMiddleware: (fetchImpl) => async (input, init) => {
const key = input.toString();
if (cache.has(key)) {
return cache.get(key).clone();
}
const response = await fetchImpl(input, init);
cache.set(key, response.clone());
return response;
},
};
},
});
const callBackendApi = createFetchClient({
plugins: [cachingPlugin],
});
URL Helpers - Dynamic params, query strings, method prefixes.
await callApi("/users/:id", { params: { id: 123 } });
await callApi("/search", { query: { q: "test" } });
await callApi("@delete/users/123");
And so much more.
npm install @zayne-labs/callapi
import { callApi, createFetchClient } from "@zayne-labs/callapi";
// Simple
const { data } = await callApi("/api/users");
// Configured
const api = createFetchClient({
baseURL: "https://api.example.com",
retryAttempts: 2,
timeout: 10000,
onError: ({ error }) => trackError(error),
});
<script type="module">
import { callApi } from "https://esm.run/@zayne-labs/callapi";
</script>
- TypeScript-first - Full inference everywhere
- Familiar API - If you know fetch, you know CallApi
- Actually small - Zero dependencies and Under 6KB, unlike other 50kb libs in the wild
- Fast - Built on native Web APIs
- Works everywhere - Browsers, Node 18+, Deno, Bun, Cloudflare Workers
MIT © Ryan Zayne