Advanced Options

Advanced configuration options for fine-tuned control

This page covers advanced CallApi configuration options for specialized use cases and fine-tuned control over request/response handling.

Response Cloning

Enable cloneResponse to read the response multiple times in different places (e.g., in hooks and main code).

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

const callApi = createFetchClient({
	cloneResponse: true,
	onSuccess: async ({ response }) => {
		const data = await response.json();
		console.log(data);
	},
});
Automatically enabled when using dedupeStrategy: "defer".

Custom Fetch Implementation

Replace the default fetch function for testing or using alternative HTTP clients.

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

// Mock fetch for testing
const mockFetch = async (url: string | Request | URL, init?: RequestInit) => {
	return Response.json(
		{ mocked: true },
		{
			status: 200,
			headers: { "Content-Type": "application/json" },
		}
	);
};

const callApi = createFetchClient({
	customFetchImpl: mockFetch,
});

Extra Fetch Options

Pass fetch options that aren't part of the standard RequestInit type with extraFetchOptions, such as Next.js's next object or Undici's dispatcher in Node.js. CallApi spreads them into the options it passes to fetch.

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

const callApi = createFetchClient({
	baseURL: "https://api.example.com",
});

// Next.js: cache the response and revalidate it every 60 seconds
await callApi("/posts", {
	extraFetchOptions: { next: { revalidate: 60 } },
});

Set it in the base config to apply it to every request. Per-request values are spread over the base values.

Debug Warnings

debugMode controls development warnings logged to the console. It defaults to true. Currently it warns when a request resolves to a relative URL, which fails during server-side rendering unless you set an absolute baseURL.

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

const callApi = createFetchClient({
	debugMode: false,
});

debugMode doesn't affect abort and timeout errors. Those are logged with console.error whenever throwOnError is off.

Skip Auto-Merge

Control which configuration parts skip automatic merging between base and instance configs.

How it works:

By default, CallApi automatically merges base config with instance config. When you set skipAutoMergeFor, CallApi stops merging that part - you become responsible for manually spreading the skipped object to preserve instance values.

This is useful when you need to manually spread instance options and then selectively override specific nested properties with defaults.

Available options:

  • "options" - Skips auto-merge of extra options (plugins, hooks, meta, etc.). You must manually spread ctx.options.
  • "request" - Skips auto-merge of request options (headers, body, method, etc.). You must manually spread ctx.request.
  • "all" - Skips auto-merge of both. You must manually spread both ctx.options and ctx.request.
api.ts
import { createFetchClient } from "@zayne-labs/callapi";

const client = createFetchClient((ctx) => ({
	baseURL: "https://api.example.com",
	plugins: [authPlugin()],
	skipAutoMergeFor: "options",

	// Spread instance options first
	...(ctx.options as object),

	// Then provide defaults for nested properties
	meta: {
		...ctx.options.meta,
		auth: {
			signInRoute: "/auth/signin",
			// Instance values override these defaults
			...ctx.options.meta?.auth,
		},
	},
}));

// Instance can override nested auth properties
await client("/protected", {
	meta: {
		auth: {
			redirectOnError: false,
		},
	},
});

Why use skipAutoMergeFor: "options"?

Without it, CallApi automatically merges ctx.options with your base config, which means you can't provide defaults for nested properties that can be overridden.

With skipAutoMergeFor: "options":

  1. CallApi stops automatically merging ctx.options
  2. You must manually spread ctx.options to preserve instance values: ...(ctx.options as object)
  3. Then you can provide defaults for nested properties
  4. Instance-provided nested values override your defaults because they're spread last

If you don't manually spread the skipped object, instance values will be lost!

Body Serialization

Customize how plain object request bodies are serialized into strings.

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

const callApi = createFetchClient({
	bodySerializer: (body) => JSON.stringify(body, null, 2),
});

Body Transformation

Transform request bodies into another Fetch-compatible body type. bodyTransformer takes precedence over bodySerializer.

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

const callApi = createFetchClient({
	bodyTransformer: ({ body }) => {
		const formData = new FormData();

		for (const [key, value] of Object.entries(body)) {
			formData.append(key, String(value));
		}

		return formData;
	},
});

Response Parsing

Customize how response strings are parsed.

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

// Parse XML responses
const xmlClient = createFetchClient({
	responseParser: (responseString) => {
		const parser = new DOMParser();
		const doc = parser.parseFromString(responseString, "text/xml");
		return xmlToObject(doc);
	},
});

// Custom JSON parser with error handling
const customClient = createFetchClient({
	responseParser: (responseString) => {
		try {
			return JSON.parse(responseString);
		} catch {
			return { error: "Invalid JSON", raw: responseString };
		}
	},
});

Default HTTP Error Messages

Customize the default error message when the server doesn't provide one.

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

const client = createFetchClient({
	defaultHTTPErrorMessage: ({ response }) => {
		switch (response.status) {
			case 401: {
				return "Authentication required";
			}
			case 403: {
				return "Access denied";
			}
			case 404: {
				return "Resource not found";
			}
			default: {
				return `Request failed with status ${response.status}`;
			}
		}
	},
});

Meta Field

Associate metadata with requests for logging, tracing, or custom handling in hooks and middleware.

Use metaHelper<T>() in the base config to type metadata for one callApi instance. It adds no runtime value:

import { createFetchClient } from "@zayne-labs/callapi";
import { metaHelper } from "@zayne-labs/callapi/utils";

const callApi = createFetchClient({
	meta: metaHelper<{ requestId?: string; source?: "background" | "dashboard" }>(),
});

await callApi("/users", { meta: { source: "dashboard" } });

Use a plain object when you want actual default metadata. Its shape does not register a type for the callApi instance.

Only use metaHelper() in the base config. It returns undefined, so passing it to an individual call sends no metadata. Pass a plain metadata object there instead.

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

const callApi = createFetchClient({
	onError: ({ options, error }) => {
		// Access metadata passed with the request
		logError({
			userId: options.meta?.userId,
			requestId: options.meta?.requestId,
			error,
		});
	},
});

await callApi("/api/data", {
	meta: {
		userId: currentUser.id,
		requestId: generateId(),
	},
});

Types

For complete type information on all options, see:

Edit on GitHub

Last updated on

On this page