A modern fetch wrapper that actually makes HTTP requests enjoyable to work with.
CallApi keeps the familiar fetch API you know, but adds the features you've always wanted: automatic retries, request deduplication, smart response parsing, and proper error handling. No more writing the same boilerplate over and over.
Drop-in replacement - Same API as fetch, just better Smart retries - Exponential backoff, custom conditions Request deduplication - No more duplicate API calls Auto response parsing - JSON, text, or binary - it just works Better error handling - Structured errors you can actually use Extensible - Hooks and plugins for custom behavior Tiny - Under 6KB, zero dependencies
import { callApi } from "@zayne-labs/callapi";
// Simple request - response type detected automatically
const { data, error } = await callApi("/api/users");
// Create a configured client
import { createFetchClient } from "@zayne-labs/callapi";
const callBackendApi = createFetchClient({
baseURL: "https://api.example.com",
retryAttempts: 2,
timeout: 10000,
});
const user = await callBackendApi("/users/123");
Prevent duplicate requests automatically:
// These will share the same request
const req1 = callApi("/api/user");
const req2 = callApi("/api/user"); // Uses result from req1
No more response.json()
everywhere:
// Automatically parsed based on Content-Type
const { data } = await callApi("/api/data"); // JSON
const { data } = await callApi("/api/page"); // HTML text
const { data } = await callApi("/api/image.png"); // Blob
const { data, error } = await callApi("/api/users");
if (error) {
console.log(error.name); // "HTTPError", "ValidationError", etc.
console.log(error.message); // Human readable message
console.log(error.errorData); // Server response data
}
// Dynamic parameters
await callApi("/users/:id/posts/:postId", {
params: { id: 123, postId: 456 },
}); // → /users/123/posts/456
// Query parameters
await callApi("/search", {
query: { q: "javascript", limit: 10 },
}); // → /search?q=javascript&limit=10
Runtime validation with your favorite library:
import { z } from "zod";
import { defineSchema, createFetchClient } from "@zayne-labs/callapi";
// Client-level validation with route schemas
const callBackendApi = createFetchClient({
baseURL: "https://api.example.com",
schema: defineSchema({
"/users/:id": {
data: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
}),
},
"/posts": {
data: z.array(
z.object({
id: z.number(),
title: z.string(),
})
),
},
}),
});
// Automatically validated based on route (both at runtime and at the type level)
const user = await callBackendApi("/users/123"); // Typed as { id: number, name: string, email: string }
const posts = await callBackendApi("/posts"); // Typed as Array<{ id: number, title: string }>
// Per-request validation
import { callApi } from "@zayne-labs/callapi";
const userSchema = z.object({
id: z.number(),
name: z.string(),
});
const { data } = await callApi("/api/user", {
schema: { data: userSchema }, // Validates response
});
// data is now typed as { id: number, name: string }
Full documentation and examples →
# npm
npm install @zayne-labs/callapi
# pnpm
pnpm add @zayne-labs/callapi
Then you can use it by importing it in your JavaScript file.
import { callApi } from "@zayne-labs/callapi";
You can import callApi directly into JavaScript through a CDN.
To do this, you first need to set your script
's type to module
, then import callApi
.
<script type="module">
import { callApi } from "https://esm.run/@zayne-labs/callapi";
</script>
<!-- Locked to a specific version -->
<script type="module">
import { callApi } from "https://esm.run/@zayne-labs/callapi@1.10.3";
</script>