Skip to main content

Installation

Install CallApi using your preferred package manager:
npm install @zayne-labs/callapi

Your First Request

The simplest way to use CallApi is with the callApi function:
import { callApi } from "@zayne-labs/callapi";

const { data, error } = await callApi("/api/users");

if (error) {
  console.error("Request failed:", error.message);
} else {
  console.log("Users:", data);
}
By default, CallApi uses resultMode: "result" which returns an object with { data, error, response }. No thrown errors unless you want them!

Making Different Request Types

1

GET Request

GET requests are the default. Just pass a URL:
const { data } = await callApi("/api/users");
With query parameters:
const { data } = await callApi("/api/users", {
  query: { page: 1, limit: 10 },
});
// → GET /api/users?page=1&limit=10
2

POST Request

Send JSON data:
const { data, error } = await callApi("/api/users", {
  method: "POST",
  body: {
    name: "John Doe",
    email: "[email protected]",
  },
});
Or use the method prefix shorthand:
const { data, error } = await callApi("@post/api/users", {
  body: {
    name: "John Doe",
    email: "[email protected]",
  },
});
3

PUT/PATCH Request

Update resources:
const { data } = await callApi("@put/api/users/:id", {
  params: { id: 123 },
  body: {
    name: "Jane Doe",
  },
});
// → PUT /api/users/123
4

DELETE Request

Delete resources:
const { data } = await callApi("@delete/api/users/:id", {
  params: { id: 123 },
});
// → DELETE /api/users/123

Creating a Configured Client

For reusable configuration, create a client with createFetchClient:
import { createFetchClient } from "@zayne-labs/callapi";

const api = createFetchClient({
  baseURL: "https://api.example.com",
  headers: {
    "Content-Type": "application/json",
  },
  timeout: 10000, // 10 seconds
});

// Now use it throughout your app
const { data } = await api("/users");
// → GET https://api.example.com/users
All options set in createFetchClient can be overridden on a per-request basis.

Adding Authentication

CallApi has built-in authentication helpers:
const api = createFetchClient({
  baseURL: "https://api.example.com",
  auth: {
    type: "Bearer",
    value: "your-token-here",
  },
});

// Or use a function for dynamic tokens
const api = createFetchClient({
  baseURL: "https://api.example.com",
  auth: {
    type: "Bearer",
    value: () => localStorage.getItem("token"),
  },
});

Handling Errors

CallApi provides structured error handling:
import { callApi, HTTPError, ValidationError } from "@zayne-labs/callapi";

const { data, error } = await callApi("/api/users");

if (error) {
  if (error instanceof HTTPError) {
    // HTTP error (4xx, 5xx)
    console.error("HTTP Error:", error.response.status);
    console.error("Error data:", error.errorData);
  } else if (error instanceof ValidationError) {
    // Schema validation failed
    console.error("Validation failed:", error.errorData);
  } else {
    // Network error, timeout, etc.
    console.error("Request error:", error.message);
  }
}
Want errors to throw instead? Set throwOnError: true in your config or per-request.

Adding Retry Logic

Automatic retries with exponential backoff:
const { data } = await callApi("/api/data", {
  retryAttempts: 3,
  retryStrategy: "exponential",
  retryDelay: 1000, // Base delay: 1 second
  retryStatusCodes: [408, 429, 500, 502, 503, 504],
});
With custom retry logic:
const { data } = await callApi("/api/data", {
  retryAttempts: 3,
  shouldRetry: ({ error, response, attemptCount }) => {
    // Custom retry logic
    if (attemptCount >= 3) return false;
    if (response?.status === 429) return true;
    return false;
  },
});

TypeScript Support

CallApi is TypeScript-first with full type inference:
import { callApi } from "@zayne-labs/callapi";

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

const { data, error } = await callApi<User>("/api/users/1");

if (data) {
  console.log(data.name); // Fully typed!
}
For runtime validation, use schemas:
import { createFetchClient } from "@zayne-labs/callapi";
import { defineSchema } from "@zayne-labs/callapi/utils";
import { z } from "zod";

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

const api = createFetchClient({
  baseURL: "https://api.example.com",
  schema: defineSchema({
    "/users/:id": {
      data: userSchema,
    },
  }),
});

const { data } = await api("/users/:id", {
  params: { id: 1 },
});
// data is typed AND validated at runtime

Request Deduplication

By default, CallApi cancels duplicate in-flight requests:
// User clicks button multiple times
const req1 = callApi("/api/user"); // Starts request
const req2 = callApi("/api/user"); // Cancels req1, starts new request
const req3 = callApi("/api/user"); // Cancels req2, starts new request
Or share the response between duplicate requests:
const api = createFetchClient({
  dedupeStrategy: "defer",
});

const req1 = api("/api/user"); // Starts request
const req2 = api("/api/user"); // Waits for req1, shares result
const req3 = api("/api/user"); // Waits for req1, shares result

const [result1, result2, result3] = await Promise.all([req1, req2, req3]);
// All three get the same response

Using Lifecycle Hooks

Hook into the request lifecycle:
const api = createFetchClient({
  baseURL: "https://api.example.com",
  
  onRequest: ({ request }) => {
    console.log("→ Sending request to:", request.url);
  },
  
  onSuccess: ({ data, response }) => {
    console.log("✓ Success! Status:", response.status);
  },
  
  onError: ({ error }) => {
    console.error("✗ Request failed:", error.message);
  },
});
Hooks can be async and are composable. Multiple hooks of the same type will be executed in order.

Advanced: Custom Response Types

Handle different response types:
const { data: imageBlob } = await callApi("/api/avatar.png", {
  responseType: "blob",
});

const imageUrl = URL.createObjectURL(imageBlob);

Next Steps

API Reference

Explore all configuration options and methods

Advanced Features

Learn about plugins, middlewares, and advanced patterns

Schema Validation

Master type safety with schema validation

Integrations

Integrate with React Query and more

Common Patterns

Global Error Handling

const api = createFetchClient({
  baseURL: "https://api.example.com",
  onError: ({ error }) => {
    // Send to error tracking service
    if (window.Sentry) {
      Sentry.captureException(error);
    }
    
    // Show user-friendly error
    if (error instanceof HTTPError) {
      showToast(`Server error: ${error.response.status}`);
    } else {
      showToast("Network error. Please try again.");
    }
  },
});

Request/Response Logging

const api = createFetchClient({
  baseURL: "https://api.example.com",
  onRequest: ({ request, options }) => {
    console.group(`→ ${request.method} ${options.fullURL}`);
    console.log("Headers:", Object.fromEntries(request.headers));
    console.groupEnd();
  },
  onResponse: ({ response, data, error }) => {
    console.group(`← ${response.status} ${response.statusText}`);
    console.log("Data:", data);
    if (error) console.error("Error:", error);
    console.groupEnd();
  },
});

Dynamic Base URL

const api = createFetchClient({
  baseURL: () => {
    // Use different API based on environment
    return process.env.NODE_ENV === "production"
      ? "https://api.production.com"
      : "https://api.staging.com";
  },
});

Automatic Token Refresh

const api = createFetchClient({
  baseURL: "https://api.example.com",
  auth: {
    type: "Bearer",
    value: async () => {
      const token = localStorage.getItem("token");
      const isExpired = checkIfTokenExpired(token);
      
      if (isExpired) {
        const newToken = await refreshToken();
        localStorage.setItem("token", newToken);
        return newToken;
      }
      
      return token;
    },
  },
});

Build docs developers (and LLMs) love