Hooks
Learn how to use hooks in CallApi
Hooks are callback functions that let you observe and react to events during the request lifecycle. They're perfect for logging, metrics, error handling, and other side effects that don't need to control the request flow.
Hooks vs Middleware: Hooks observe events, so naturally their return values are ignored. Middleware on the other hand, controls execution flow and can modify or replace operations. Use hooks for side effects, middleware for control flow. See Middleware for details.
You can configure hook execution using hooksExecutionMode (parallel vs sequential). Plugin hooks
always execute before main hooks.
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
},
: () => {
// 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
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...
},
});Manual Refetching
The error-related hooks all receive an options object that contains a refetch function.
This allows you to manually trigger a retry of the original request directly from an error hook. It's particularly useful for recovery flows like authentication token refreshes.
const client = createFetchClient({
onResponseError: async ({ response, options }) => {
if (response.status === 401) {
// Refresh session logic
const newToken = await refreshToken();
localStorage.setItem("token", newToken);
// Retry request with new token
options.refetch();
}
},
});For a detailed walkthrough on using refetch() for token refreshing, see the Authorization guide.
To prevent infinite loops, manual refetches are limited by the refetchAttempts option (defaults to
1). If the limit is reached, refetch() will return null and log an error to the console instead of
retrying recursively.
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.2 Base, 2.3 Base, 3.2 Instance]is then executed.
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