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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
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.

example.ts
import {  } from "@zayne-labs/callapi";

const  = ({
	: ({ , , ,  }) => {
		// Handle stuff...
	},
});

Ways in which hooks can be provided

Hooks can be provided at three levels:

  1. The Plugin Level: (covered in plugins)
  2. The Base Client Level: (createFetchClient)
  3. The Instance Level: (callApi)

And each hook can be provided, as:

  • A single callback function.
  • An array of callback functions.
example.ts
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

example.ts
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 → 3

Use 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 via Promise.all() for better performance
  • "sequential": All hooks execute one by one in registration order via await in a loop
Hook Execution Mode Examples
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

  1. 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.

  2. Instance Hooks: Instance-level hooks generally override base client hooks if both are single functions.

  3. 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.

Hook Order and Merging Example
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 onRequest and onResponse run first.
  • The base client's onRequest is a single function, and the instance provides its own onRequest. The instance hook replaces the base hook for this specific call.
  • The base client's onResponse is an array. The instance onResponse is 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.

example.ts
import {  } from "@zayne-labs/callapi";

type  = {
	: number;
	: string;
};

const  = <{ :  }>({
	: ({  }) => {
		.(.);
	},
});

const {  } = await ("/api/data", {
	: ({  }) => {
		.(.);
	},
});

Hover over the data object to see the inferred type

Streaming

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 bytes
  • transferredBytes: Amount of data transferred so far

Types

Prop

Type

Edit on GitHub

Last updated on

On this page