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
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
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] " ,
},
});
PUT/PATCH Request
Update resources: const { data } = await callApi ( "@put/api/users/:id" , {
params: { id: 123 },
body: {
name: "Jane Doe" ,
},
});
// → PUT /api/users/123
DELETE Request
Delete resources: const { data } = await callApi ( "@delete/api/users/:id" , {
params: { id: 123 },
});
// → DELETE /api/users/123
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:
Bearer Token
Basic Auth
Custom Auth
Shorthand
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" ),
},
});
const api = createFetchClient ({
baseURL: "https://api.example.com" ,
auth: {
type: "Basic" ,
username: "user" ,
password: "pass123" ,
},
});
const api = createFetchClient ({
baseURL: "https://api.example.com" ,
auth: {
type: "Custom" ,
prefix: "Token" ,
value: "your-token-here" ,
},
});
// → Authorization: Token your-token-here
// Defaults to Bearer token
const api = createFetchClient ({
baseURL: "https://api.example.com" ,
auth: "your-token-here" ,
});
// → Authorization: Bearer your-token-here
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 );
const { data : stream } = await callApi ( "/api/large-file" , {
responseType: "stream" ,
onResponseStream : ({ event }) => {
console . log ( `Downloaded: ${ event . loaded } / ${ event . total } bytes` );
console . log ( `Progress: ${ event . progress } %` );
},
});
const { data : buffer } = await callApi ( "/api/binary" , {
responseType: "arrayBuffer" ,
});
const uint8Array = new Uint8Array ( buffer );
const { data : text } = await callApi ( "/api/readme" , {
responseType: "text" ,
});
console . log ( text );
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 ;
},
},
});