Hooks
Learn how to use hooks in CallApi
Hooks are callback functions that let you observe and react to events during the request lifecycle. Think of them like event listeners for your API requests.
CallApi runs these functions (sync or async) automatically at specific points – before sending, after receiving a response, when an error occurs, and so on. Hooks are great for logging, metrics, error handling, and side effects.
Hooks vs Middleware: Hooks observe and react to events, but their return values are ignored. If you need explicit control over execution flow (return a custom response, short-circuit operations etc.), use Middleware instead.
You can configure how hooks execute using hooksExecutionMode (parallel vs sequential). Plugin hooks always execute before main hooks. See the Hook Configuration Options section for details.
import { } from "@zayne-labs/callapi";
const = ({
: "http://localhost:3000",
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
});
("/api/data", {
: () => {},
: () => {},
: () => {},
: () => {},
: () => {},
: () => {},
: () => {},
: () => {},
: () => {},
: () => {},
: () => {},
});What hooks are available and when do they run?
Request Phase Hooks
onRequest
This hook is called before the final request processing (body serialization, authentication, etc.) but after URL resolution. You can use this to add headers, handle authentication, or modify request configuration.
import { } from "@zayne-labs/callapi";
const = ({
: ({ , }) => {
// Add auth header
. = .("token");
// Add custom headers
.["X-Custom-ID"] = "123";
// Add environment header based on baseURL
if (.?.("api.dev")) {
.["X-Environment"] = "development";
}
},
});onRequestReady
This hook is called just before the HTTP request is sent and after the request has been fully processed internally (including auth headers and body serialization). Use this when you need to inspect or log the final request state.
import { } from "@zayne-labs/callapi";
const = ({
: ({ }) => {
// Final check of the request object
.("Final Headers:", .);
.("Final Body:", .);
},
});onRequestStream
This hook is called during request body streaming, useful for tracking upload progress.
import { createFetchClient } from "@zayne-labs/callapi";
const client = createFetchClient({
onRequestStream: ({ event }) => {
// Access stream progress information
console.log(`Upload progress: ${event.progress}%`);
console.log(`Bytes transferred: ${event.transferredBytes}`);
console.log(`Total bytes: ${event.totalBytes}`);
// Access the current chunk being streamed
// event.chunk is a Uint8Array
},
});
// Example: Uploading a large file
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput?.files?.[0];
if (file) {
await client("/api/upload", {
method: "POST",
body: file,
onRequestStream: ({ event }) => {
updateUploadProgress(event.progress);
},
});
}onRequestError
This hook is called when the request fails before reaching the server. You can use it to handle network errors, timeouts, etc.
import { } from "@zayne-labs/callapi";
const = ({
: ({ , , }) => {
if (. === "TimeoutError") {
.(`Request timeout: ${.}`);
return;
}
.(`Network error: ${.}`);
},
});Response Phase Hooks
onResponse
This hook is called for every response from the server, regardless of the status code. You can use it to log all API calls, handle specific status codes, etc.
import { } from "@zayne-labs/callapi";
const = ({
: ({ , , , , }) => {
// Log all API calls
.(`${.} ${.} - ${?.}`);
// Handle specific status codes
if (?. === 207) {
.("Partial success:", );
}
},
});onResponseStream
This hook is called during response body streaming, perfect for tracking download progress.
import { createFetchClient } from "@zayne-labs/callapi";
const client = createFetchClient({
onResponseStream: ({ event }) => {
// Access stream progress information
console.log(`Download progress: ${event.progress}%`);
console.log(`Bytes received: ${event.transferredBytes}`);
console.log(`Total bytes: ${event.totalBytes}`);
// Process the current chunk
// event.chunk is a Uint8Array
},
});
// Example: Downloading a large file
const { data } = await client("/api/download-video", {
responseType: "stream",
onResponseStream: ({ event }) => {
updateDownloadProgress(event.progress);
},
});onSuccess
This hook is called only for successful responses. You can use it to handle successful responses, cache data, etc.
import { } from "@zayne-labs/callapi";
type = {
: string;
: string;
: string;
};
const = new <string, >();
const = <{ : [] }>({
: ({ , , , }) => {
// Cache user data
.(() => .(., ));
},
});onResponseError
This hook is called for error responses (response.ok === false). You can use it to handle specific status codes, etc.
import { } from "@zayne-labs/callapi";
const = ({
: ({ , , }) => {
switch (.) {
case 401: {
// Handle Token expired
break;
}
case 403: {
// Handle User not authorized
break;
}
case 404: {
// Handle Resource not found
break;
}
case 429: {
// Handle Rate limited
break;
}
default: {
// Handle other errors
break;
}
}
},
});onError
Called for any error .
This hook is called for any error. It's basically a combination of onRequestError and onResponseError. It's perfect for global error handling.
import { } from "@zayne-labs/callapi";
const = ({
: ({ , , , }) => {
// Send to error tracking
// errorTracker.capture({
// type: error.name,
// message: error.message,
// url: request.url,
// status: response?.status,
// });
// Show user-friendly messages
if (!) {
// showNetworkError();
} else if (. >= 500) {
// showServerError();
} else if (. === 400) {
// showValidationErrors(error.errorData);
}
},
});Retry Phase Hooks
onRetry
This hook is called before retrying a failed request. You can use it to handle stuff before retrying.
import { } from "@zayne-labs/callapi";
const = ({
// Advanced retry configuration
: 3,
: "exponential",
: [408, 429, 500, 502, 503, 504],
: ({ }) => {
// Handle stuff...
},
});Validation Phase Hooks
onValidationError
This hook is called when request or response validation fails via the schema option.
import { } from "@zayne-labs/callapi";
const = ({
: ({ , , , }) => {
// Handle stuff...
},
});Ways in which hooks can be provided
Hooks can be provided at three levels:
- The Plugin Level: (covered in
plugins) - The Base Client Level: (
createFetchClient) - The Instance Level: (
callApi)
And each hook can be provided, as:
- A single callback function.
- An array of callback functions.
import { createFetchClient } from "@zayne-labs/callapi";
const callApi = createFetchClient({
onRequest: [
(ctx) => addAuthHeader(ctx.request),
(ctx) => addTrackingHeader(ctx.request),
(ctx) => addVersionHeader(ctx.request),
],
});
const result = await callApi("/api/data", {
onRequest: (ctx) => {},
});Hook Execution Order
Hooks execute in the following order:
Plugin Hooks → Base Client Hooks → Instance Hooks
import { createFetchClient } from "@zayne-labs/callapi";
import { definePlugin } from "@zayne-labs/callapi/utils";
// Plugin hooks execute first
const loggingPlugin = definePlugin({
id: "logger",
name: "Logger Plugin",
hooks: {
onRequest: () => console.log("1. Plugin onRequest"),
onResponse: () => console.log("1. Plugin onResponse"),
},
});
// Base client hooks execute second
const callMainApi = createFetchClient({
plugins: [loggingPlugin],
onRequest: () => console.log("2. Base onRequest"),
onResponse: () => console.log("2. Base onResponse"),
});
// Instance hooks execute last
await callMainApi("/api/data", {
onRequest: () => console.log("3. Instance onRequest"),
onResponse: () => console.log("3. Instance onResponse"),
});
// With parallel execution (default), all hooks at each level run simultaneously
// With sequential execution, they run in order: 1 → 2 → 3Use hooksExecutionMode to control whether hooks run in parallel or sequentially. Plugin hooks always execute before base and instance hooks.
Hook Configuration Options
hooksExecutionMode
Controls whether all hooks (plugin + main + instance) execute in parallel or sequentially.
"parallel"(default): All hooks execute simultaneously viaPromise.all()for better performance"sequential": All hooks execute one by one in registration order viaawaitin a loop
import { createFetchClient } from "@zayne-labs/callapi";
// Parallel execution (default) - all hooks run simultaneously
const parallelClient = createFetchClient({
hooksExecutionMode: "parallel", // Default
onRequest: [
async (ctx) => await addAuthToken(ctx.request), // Runs in parallel
async (ctx) => await logRequest(ctx.request), // Runs in parallel
async (ctx) => await addTrackingId(ctx.request), // Runs in parallel
],
});
// Sequential execution - hooks run one after another
const sequentialClient = createFetchClient({
hooksExecutionMode: "sequential",
onRequest: [
async (ctx) => await validateAuth(ctx.request), // Runs first
async (ctx) => await transformRequest(ctx.request), // Runs second
async (ctx) => await logRequest(ctx.request), // Runs third
],
});
// Use case: Hooks have dependencies and must run in order
const dependentClient = createFetchClient({
hooksExecutionMode: "sequential",
onError: [
(ctx) => logError(ctx.error), // Log first
(ctx) => reportError(ctx.error), // Then report
(ctx) => cleanupResources(ctx), // Finally cleanup
],
});Hook Overriding And Merging
-
Plugin Hooks: These run first by default and can't be overridden by either instance or base client hooks, making them perfect for must-have functionality.
-
Instance Hooks: Instance-level hooks generally override base client hooks if both are single functions.
-
Base Client Hooks: When the base client hook is an array, instance hooks are merged into that array instead of replacing it, allowing you to add additional functionality without overriding existing logic.
This merging behavior for array-type base client hooks is mostly just a convenience. If you need more sophisticated control or guaranteed execution, defining your logic within a dedicated plugin is often the better approach.
import { createFetchClient } from "@zayne-labs/callapi";
import { definePlugin } from "@zayne-labs/callapi/utils";
// 1. Plugin Hook (Runs first)
const examplePlugin = definePlugin({
id: "example-plugin",
name: "ExamplePlugin",
hooks: {
onRequest: (ctx) => console.log("1.1 Plugin onRequest -- (cannot be overridden)"),
onResponse: (ctx) => console.log("1.2 Plugin onResponse -- (cannot be overridden)"),
},
});
// 2. Base Client Hooks (Runs second)
const callBackendApi = createFetchClient({
plugins: [examplePlugin],
// Single base hook - will be overridden by instance hook
onRequest: (ctx) =>
console.log("2.1 Base onRequest -- (will be overridden by instance onRequest hook (3.1))"),
// Array base hook - instance hook will be merged
onResponse: [
(ctx) => console.log("2.2 Base onResponse (part of array)"),
(ctx) => console.log("2.3 Base onResponse (part of array)"),
],
});
// 3. Instance Hooks (Runs last, overrides or merges)
const result = await callBackendApi("/data", {
// Overrides the single base onRequest hook
onRequest: (ctx) => console.log("3.1 Instance onRequest (overrides base onRequest (2.1))"),
// Merges with the base onResponse array
onResponse: (ctx) => console.log("3.2 Instance onResponse (merged with base onResponse (2.2, 2.3))"),
});Explanation:
- The plugin's
onRequestandonResponserun first. - The base client's
onRequestis a single function, and the instance provides its ownonRequest. The instance hook replaces the base hook for this specific call. - The base client's
onResponseis an array. The instanceonResponseis added to this array. The combined array[2.1 Base, 2.2 Base, 3.1 Instance]is then executed sequentially.
Async Hooks
All hooks can be async or return a Promise. When this is the case, the hook will be awaited internally:
onRequest: async ({ request }) => {
const token = await getAuthToken();
request.headers.Authorization = `Bearer ${token}`;
};Type Safety
All hooks are fully typed based on the response type you specify when creating the client. This ensures you get proper type inference and autocompletion for the data and error objects in all your hooks.
import { } from "@zayne-labs/callapi";
type = {
: number;
: string;
};
const = <{ : }>({
: ({ }) => {
.(.);
},
});
const { } = await ("/api/data", {
: ({ }) => {
.(.);
},
});
Hover over the data object to see the inferred typeStreaming
Both stream hooks (onRequestStream and onResponseStream) receive a context object with an event property of type StreamProgressEvent that contains:
chunk: Current chunk of data being streamed (Uint8Array)progress: Progress percentage (0-100)totalBytes: Total size of data in bytestransferredBytes: Amount of data transferred so far
Types
Prop
Type
Last updated on