# CallApi: Advanced Options URL: /docs/advanced-options Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/advanced-options.mdx 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 [#response-cloning] Enable `cloneResponse` to read the response multiple times in different places (e.g., in hooks and main code). ```ts title="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 [#custom-fetch-implementation] Replace the default fetch function for testing or using alternative HTTP clients. ```ts title="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, }); ``` Skip Auto-Merge [#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`. ```ts title="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 [#body-serialization] Customize how request body objects are serialized. ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callApi = createFetchClient({ bodySerializer: (data) => { // Convert object to URL-encoded form data const formData = new URLSearchParams(); Object.entries(data).forEach(([key, value]) => { formData.append(key, String(value)); }); return formData.toString(); }, }); ``` Response Parsing [#response-parsing] Customize how response strings are parsed. ```ts title="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 [#default-http-error-messages] Customize the default error message when the server doesn't provide one. ```ts title="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 [#meta-field] Associate metadata with requests for logging, tracing, or custom handling in hooks and middleware. ```ts title="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 [#types] For complete type information on all options, see: * [Extra Options](/docs/extra-options) - Instance-level options * [Base Extra Options](/docs/base-extra-options) - Base client options * [Request Options](/docs/request-options) - Fetch API options # CallApi: Authorization URL: /docs/authorization Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/authorization.mdx Learn how to use convenience authorization helpers callApi provides CallApi provides convenient authorization helpers via the `auth` property, making it easy to add authentication headers to your requests without manually constructing Authorization headers. **Security Best Practice:** Never hardcode sensitive tokens or credentials in your source code. Always use environment variables, secure storage, or runtime token retrieval functions. Bearer [#bearer] Since Bearer is the most common authorization type, passing a string to `auth` automatically generates a `Bearer` Authorization header. You can also use an object with a `bearer` property. ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; // Passing a string const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", auth: "my-token", // [!code highlight] }); // Passing an object const result = await callBackendApi("/users/123", { auth: { type: "Bearer", value: "my-token", }, }); ``` The above is equivalent to writing the following with Fetch: ```ts fetch("http://localhost:3000/users/123", { headers: { Authorization: `Bearer my-token`, }, }); ``` You can also pass a function that returns a string or a promise that resolves to a string. This is useful for retrieving tokens dynamically: ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", }); const result = callBackendApi("/users/123", { auth: { type: "Bearer", value: () => authStore.getToken(), }, }); ``` The function will be called only once when the request is made. If it returns undefined or null, the header will not be added to the request. This allows for conditional authentication. Token [#token] Similar to Bearer authorization, but uses `Token` as the header prefix instead of `Bearer`. ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", auth: { type: "Token", value: "my-token", }, }); const result = await callBackendApi("/users/123"); ``` The above is equivalent to writing the following with Fetch: ```ts fetch("http://localhost:3000/users/123", { headers: { Authorization: `Token my-token`, }, }); ``` Basic [#basic] Basic authentication adds username and password to the `Authorization` header, automatically base64 encoded. ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", auth: { type: "Basic", username: "my-username", password: "my-password", }, }); ``` You can also pass async getter functions for the username and password fields. ```ts const { data } = await callApi("/api/data", { auth: { type: "Basic", username: async () => await getUsername(), password: async () => await getPassword(), }, }); ``` Custom [#custom] For custom authorization schemes not supported by default, use the `Custom` type with `prefix` and `value` properties. ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", auth: { type: "Custom", prefix: "SomePrefix", value: "my-token", }, }); const result = await callBackendApi("/users/123"); ``` The above is equivalent to writing the following with Fetch: ```ts fetch("http://localhost:3000/users/123", { headers: { Authorization: `SomePrefix my-token` }, }); ``` Advanced Examples [#advanced-examples] Token Refresh [#token-refresh] Combine `auth` with the `onError` hook and `retry` behavior to implement automatic token refreshing: ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", // The auth function is called on every request, ensuring we use a fresh token auth: () => getAccessToken(), // We can automatically retry failed requests that were unauthorized onResponseError: async ({ response, options }) => { if (response.status === 401) { // Force a network refresh to get a new token await refreshAccessToken(); // Retry the original request with the new token options.retryAttempts = 1; } }, }); ``` Conditional Auth [#conditional-auth] Conditionally trigger authentication by returning `null` or `undefined` from your auth function when the user is not authenticated: ```ts title="api.ts" const client = createFetchClient({ baseURL: "https://api.example.com", auth: async () => { // Only add auth if user is logged in const isLoggedIn = await checkAuthStatus(); // Returning null means no auth header will be added return isLoggedIn ? await getToken() : null; }, }); // Or dynamically per-request async function makeRequest(endpoint: string, requiresAuth: boolean) { return callApi(endpoint, { auth: requiresAuth ? () => getToken() : undefined, }); } ``` Global Auth with Override [#global-auth-with-override] Define a global authentication strategy when creating a client and override or disable it for specific requests: ```ts title="api.ts" const client = createFetchClient({ baseURL: "https://api.example.com", auth: () => getSessionToken(), }); // Uses the global auth token const userData = await client("/api/user"); // Override with a different token for a specific request const adminData = await client("/api/admin", { auth: () => getAdminToken(), }); // Disable auth entirely for a specific request const publicData = await client("/api/public", { auth: undefined, }); ``` # CallApi: Base Extra Options URL: /docs/base-extra-options Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/base-extra-options.mdx Options that can be passed to the createFetchClient function This page documents the configuration options specific to `createFetchClient`. These options define the base behavior for all requests made with the created client instance. ; ResultMode: ResultModeType; }>>) => unknown) | (((context: ErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequest", "description": "Hook called before the HTTP request is sent and before any internal processing of the request object begins.\n\nThis is the ideal place to modify request headers, add authentication,\nimplement request logging, or perform any setup before the network call.", "tags": [ { "name": "param", "text": "context - Request context with mutable request object and configuration" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequestError", "description": "Hook called when an error occurs during the fetch request itself.\n\nThis handles network-level errors like connection failures, timeouts,\nDNS resolution errors, or other issues that prevent getting an HTTP response.\nNote that HTTP error status codes (4xx, 5xx) are handled by `onResponseError`.", "tags": [ { "name": "param", "text": "context - Request error context with error details and null response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestErrorContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequestReady", "description": "Hook called just before the HTTP request is sent and after the request has been processed.", "tags": [ { "name": "param", "text": "context - Request context with mutable request object and configuration" } ], "type": "((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequestStream", "description": "Hook called during upload stream progress tracking.\n\nThis hook is triggered when uploading data (like file uploads) and provides\nprogress information about the upload. Useful for implementing progress bars\nor upload status indicators.", "tags": [ { "name": "param", "text": "context - Request stream context with progress event and request instance" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestStreamContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestStreamContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onResponse", "description": "Hook called when any HTTP response is received from the API.\n\nThis hook is triggered for both successful (2xx) and error (4xx, 5xx) responses.\nIt's useful for response logging, metrics collection, or any processing that\nshould happen regardless of response status.", "tags": [ { "name": "param", "text": "context - Response context with either success data or error information" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ResponseContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onResponseError", "description": "Hook called when an HTTP error response (4xx, 5xx) is received from the API.\n\nThis handles server-side errors where an HTTP response was successfully received\nbut indicates an error condition. Different from `onRequestError` which handles\nnetwork-level failures.", "tags": [ { "name": "param", "text": "context - Response error context with HTTP error details and response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseErrorContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ResponseErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onResponseStream", "description": "Hook called during download stream progress tracking.\n\nThis hook is triggered when downloading data (like file downloads) and provides\nprogress information about the download. Useful for implementing progress bars\nor download status indicators.", "tags": [ { "name": "param", "text": "context - Response stream context with progress event and response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseStreamContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ResponseStreamContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRetry", "description": "Hook called when a request is being retried.\n\nThis hook is triggered before each retry attempt, providing information about\nthe previous failure and the current retry attempt number. Useful for implementing\ncustom retry logic, exponential backoff, or retry logging.", "tags": [ { "name": "param", "text": "context - Retry context with error details and retry attempt count" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RetryContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RetryContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onSuccess", "description": "Hook called when a successful response (2xx status) is received from the API.\n\nThis hook is triggered only for successful responses and provides access to\nthe parsed response data. Ideal for success logging, caching, or post-processing\nof successful API responses.", "tags": [ { "name": "param", "text": "context - Success context with parsed response data and response object" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: SuccessContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: SuccessContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onValidationError", "description": "Hook called when a validation error occurs.\n\nThis hook is triggered when request or response data fails validation against\na defined schema. It provides access to the validation error details and can\nbe used for custom error handling, logging, or fallback behavior.", "tags": [ { "name": "param", "text": "context - Validation error context with error details and response (if available)" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ValidationErrorContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ValidationErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "timeout", "description": "Request timeout in milliseconds. Request will be aborted if it takes longer.\n\nUseful for preventing requests from hanging indefinitely and providing\nbetter user experience with predictable response times.", "tags": [ { "name": "example", "text": "```ts\n// 5 second timeout\ntimeout: 5000\n\n// Different timeouts for different endpoints\nconst quickApi = createFetchClient({ timeout: 3000 }); // 3s for fast endpoints\nconst slowApi = createFetchClient({ timeout: 30000 }); // 30s for slow operations\n\n// Per-request timeout override\nawait callApi(\"/quick-data\", { timeout: 1000 });\nawait callApi(\"/slow-report\", { timeout: 60000 });\n\n// No timeout (use with caution)\ntimeout: 0\n```" } ], "type": "number | undefined", "simplifiedType": "number", "required": false, "deprecated": false }, { "name": "retryAttempts", "description": "Number of allowed retry attempts on HTTP errors", "tags": [ { "name": "default", "text": "0" } ], "type": "number | undefined", "simplifiedType": "number", "required": false, "deprecated": false }, { "name": "retryCondition", "description": "Callback whose return value determines if a request should be retried or not", "tags": [], "type": "RetryCondition | undefined", "simplifiedType": "RetryCondition", "required": false, "deprecated": false }, { "name": "retryDelay", "description": "Delay between retries in milliseconds", "tags": [ { "name": "default", "text": "1000" } ], "type": "number | ((currentAttemptCount: number) => number) | undefined", "simplifiedType": "function | number", "required": false, "deprecated": false }, { "name": "retryMaxDelay", "description": "Maximum delay in milliseconds. Only applies to exponential strategy", "tags": [ { "name": "default", "text": "10000" } ], "type": "number | undefined", "simplifiedType": "number", "required": false, "deprecated": false }, { "name": "retryMethods", "description": "HTTP methods that are allowed to retry", "tags": [ { "name": "default", "text": "[\"GET\", \"POST\"]" } ], "type": "(AnyString | \"CONNECT\" | \"DELETE\" | \"GET\" | \"HEAD\" | \"OPTIONS\" | \"PATCH\" | \"POST\" | \"PUT\" | \"TRACE\")[] | undefined", "simplifiedType": "array", "required": false, "deprecated": false }, { "name": "retryStatusCodes", "description": "HTTP status codes that trigger a retry", "tags": [], "type": "(AnyNumber | 408 | 409 | 425 | 429 | 500 | 502 | 503 | 504)[] | undefined", "simplifiedType": "array", "required": false, "deprecated": false }, { "name": "retryStrategy", "description": "Strategy to use when retrying", "tags": [ { "name": "default", "text": "\"linear\"" } ], "type": "\"exponential\" | \"linear\" | undefined", "simplifiedType": "\"linear\" | \"exponential\"", "required": false, "deprecated": false }, { "name": "initURL", "description": "The original URL string passed to the callApi instance (readonly)\n\nThis preserves the original URL as provided, including any method modifiers like \"@get/\" or \"@post/\".", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "dedupeCacheScope", "description": "Controls the scope of request deduplication caching.\n\n- `\"global\"`: Shares deduplication cache across all `createFetchClient` instances with the same `dedupeCacheScopeKey`.\n Useful for applications with multiple API clients that should share deduplication state.\n- `\"local\"`: Limits deduplication to requests within the same `createFetchClient` instance.\n Provides better isolation and is recommended for most use cases.\n\n\n**Real-world Scenarios:**\n- Use `\"global\"` when you have multiple API clients (user service, auth service, etc.) that might make overlapping requests\n- Use `\"local\"` (default) for single-purpose clients or when you want strict isolation between different parts of your app", "tags": [ { "name": "example", "text": "```ts\n// Local scope - each client has its own deduplication cache\nconst userClient = createFetchClient({ baseURL: \"/api/users\" });\nconst postClient = createFetchClient({ baseURL: \"/api/posts\" });\n// These clients won't share deduplication state\n\n// Global scope - share cache across related clients\nconst userClient = createFetchClient({\n baseURL: \"/api/users\",\n dedupeCacheScope: \"global\",\n});\nconst postClient = createFetchClient({\n baseURL: \"/api/posts\",\n dedupeCacheScope: \"global\",\n});\n// These clients will share deduplication state\n```" }, { "name": "default", "text": "\"local\"" } ], "type": "\"global\" | \"local\" | undefined", "simplifiedType": "\"local\" | \"global\"", "required": false, "deprecated": false }, { "name": "dedupeCacheScopeKey", "description": "Unique namespace for the global deduplication cache when using `dedupeCacheScope: \"global\"`.\n\nThis creates logical groupings of deduplication caches. All instances with the same key\nwill share the same cache namespace, allowing fine-grained control over which clients\nshare deduplication state.\n\n**Best Practices:**\n- Use descriptive names that reflect the logical grouping (e.g., \"user-service\", \"analytics-api\")\n- Keep scope keys consistent across related API clients\n- Consider using different scope keys for different environments (dev, staging, prod)\n- Avoid overly broad scope keys that might cause unintended cache sharing\n\n**Cache Management:**\n- Each scope key maintains its own independent cache\n- Caches are automatically cleaned up when no references remain\n- Consider the memory implications of multiple global scopes", "tags": [ { "name": "example", "text": "```ts\n// Group related API clients together\nconst userClient = createFetchClient({\n baseURL: \"/api/users\",\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: \"user-service\"\n});\nconst profileClient = createFetchClient({\n baseURL: \"/api/profiles\",\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: \"user-service\" // Same scope - will share cache\n});\n\n// Separate analytics client with its own cache\nconst analyticsClient = createFetchClient({\n baseURL: \"/api/analytics\",\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: \"analytics-service\" // Different scope\n});\n\n// Environment-specific scoping\nconst apiClient = createFetchClient({\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: `api-${process.env.NODE_ENV}` // \"api-development\", \"api-production\", etc.\n});\n```" }, { "name": "default", "text": "\"default\"" } ], "type": "AnyString | \"default\" | ((context: RequestContext) => string | undefined) | undefined", "simplifiedType": "function | \"default\" | AnyString", "required": false, "deprecated": false }, { "name": "dedupeKey", "description": "Custom key generator for request deduplication.\n\nOverride the default key generation strategy to control exactly which requests\nare considered duplicates. The default key combines URL, method, body, and\nrelevant headers (excluding volatile ones like 'Date', 'Authorization', etc.).\n\n**Default Key Generation:**\nThe auto-generated key includes:\n- Full request URL (including query parameters)\n- HTTP method (GET, POST, etc.)\n- Request body (for POST/PUT/PATCH requests)\n- Stable headers (excludes Date, Authorization, User-Agent, etc.)\n\n**Custom Key Best Practices:**\n- Include only the parts of the request that should affect deduplication\n- Avoid including volatile data (timestamps, random IDs, etc.)\n- Consider performance - simpler keys are faster to compute and compare\n- Ensure keys are deterministic for the same logical request\n- Use consistent key formats across your application\n\n**Performance Considerations:**\n- Function-based keys are computed on every request - keep them lightweight\n- String keys are fastest but least flexible\n- Consider caching expensive key computations if needed", "tags": [ { "name": "example", "text": "```ts\nimport { callApi } from \"@zayne-labs/callapi\";\n\n// Simple static key - useful for singleton requests\nconst config = callApi(\"/api/config\", {\n dedupeKey: \"app-config\",\n dedupeStrategy: \"defer\" // Share the same config across all requests\n});\n\n// URL and method only - ignore headers and body\nconst userData = callApi(\"/api/user/123\", {\n dedupeKey: (context) => `${context.options.method}:${context.options.fullURL}`\n});\n\n// Include specific headers in deduplication\nconst apiCall = callApi(\"/api/data\", {\n dedupeKey: (context) => {\n const authHeader = context.request.headers.get(\"Authorization\");\n return `${context.options.fullURL}-${authHeader}`;\n }\n});\n\n// User-specific deduplication\nconst userSpecificCall = callApi(\"/api/dashboard\", {\n dedupeKey: (context) => {\n const userId = context.options.fullURL.match(/user\\/(\\d+)/)?.[1];\n return `dashboard-${userId}`;\n }\n});\n\n// Ignore certain query parameters\nconst searchCall = callApi(\"/api/search?q=test×tamp=123456\", {\n dedupeKey: (context) => {\n const url = new URL(context.options.fullURL);\n url.searchParams.delete(\"timestamp\"); // Remove volatile param\n return `search:${url.toString()}`;\n }\n});\n```" }, { "name": "default", "text": "Auto-generated from request details" } ], "type": "string | ((context: RequestContext) => string | undefined) | undefined", "simplifiedType": "function | string", "required": false, "deprecated": false }, { "name": "dedupeStrategy", "description": "Strategy for handling duplicate requests. Can be a static string or callback function.\n\n**Available Strategies:**\n- `\"cancel\"`: Cancel previous request when new one starts (good for search)\n- `\"defer\"`: Share response between duplicate requests (good for config loading)\n- `\"none\"`: No deduplication, all requests execute independently", "tags": [ { "name": "example", "text": "```ts\n// Static strategies\nconst searchClient = createFetchClient({\n dedupeStrategy: \"cancel\" // Cancel previous searches\n});\n\nconst configClient = createFetchClient({\n dedupeStrategy: \"defer\" // Share config across components\n});\n\n// Dynamic strategy based on request\nconst smartClient = createFetchClient({\n dedupeStrategy: (context) => {\n return context.options.method === \"GET\" ? \"defer\" : \"cancel\";\n }\n});\n\n// Search-as-you-type with cancel strategy\nconst handleSearch = async (query: string) => {\n try {\n const { data } = await callApi(\"/api/search\", {\n method: \"POST\",\n body: { query },\n dedupeStrategy: \"cancel\",\n dedupeKey: \"search\" // Cancel previous searches, only latest one goes through\n });\n\n updateSearchResults(data);\n } catch (error) {\n if (error.name === \"AbortError\") {\n // Previous search cancelled - (expected behavior)\n return;\n }\n console.error(\"Search failed:\", error);\n }\n};\n\n```" }, { "name": "default", "text": "\"cancel\"" } ], "type": "\"cancel\" | \"defer\" | \"none\" | ((context: RequestContext) => \"cancel\" | \"defer\" | \"none\") | undefined", "simplifiedType": "function | \"none\" | \"defer\" | \"cancel\"", "required": false, "deprecated": false }, { "name": "hooksExecutionMode", "description": "Controls the execution mode of all composed hooks (main + plugin hooks).\n\n- **\"parallel\"**: All hooks execute simultaneously via Promise.all() for better performance\n- **\"sequential\"**: All hooks execute one by one in registration order via await in a loop\n\nThis affects how ALL hooks execute together, regardless of their source (main or plugin).", "tags": [ { "name": "default", "text": "\"parallel\"" } ], "type": "\"parallel\" | \"sequential\" | undefined", "simplifiedType": "\"sequential\" | \"parallel\"", "required": false, "deprecated": false }, { "name": "fetchMiddleware", "description": "Wraps the fetch implementation to intercept requests at the network layer.\n\nTakes a context object containing the current fetch function and returns a new fetch function.\nUse it to cache responses, add logging, handle offline mode, or short-circuit requests etc.\nMultiple middleware compose in order: plugins → base config → per-request.\n\nUnlike `customFetchImpl`, middleware can call through to the original fetch.", "tags": [ { "name": "example", "text": "```ts\n// Cache responses\nconst cache = new Map();\n\nfetchMiddleware: (ctx) => async (input, init) => {\n const key = input.toString();\n\n const cachedResponse = cache.get(key);\n\n if (cachedResponse) {\n return cachedResponse.clone();\n }\n\n const response = await ctx.fetchImpl(input, init);\n cache.set(key, response.clone());\n\n return response;\n}\n\n// Handle offline\nfetchMiddleware: (ctx) => async (...parameters) => {\n if (!navigator.onLine) {\n return new Response('{\"error\": \"offline\"}', { status: 503 });\n }\n\n return ctx.fetchImpl(...parameters);\n}\n```" } ], "type": "((context: FetchMiddlewareContext; ResultMode: ResultModeType; }>>) => FetchImpl) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "resultMode", "description": "Dictates how CallApi processes and returns the final result\n\n- **\"all\"** (default): Returns `{ data, error, response }`. Standard lifecycle.\n- **\"onlyData\"**: Returns only the data from the response.\n- **\"onlyResponse\"**: Returns only the `Response` object.\n- **\"fetchApi\"**: Also returns only the `Response` object, but also skips parsing of the response body internally and data/errorData schema validation.\n- **\"withoutResponse\"**: Returns `{ data, error }`. Standard lifecycle, but omits the `response` property.\n\n\n**Note:**\nBy default, simplified modes (`\"onlyData\"`, `\"onlyResponse\"`, `\"fetchApi\"`) do not throw errors.\nSuccess/failure should be handled via hooks or by checking the return value (e.g., `if (data)` or `if (response?.ok)`).\nTo force an exception instead, set `throwOnError: true`.", "tags": [ { "name": "default", "text": "\"all\"" } ], "type": "ResultModeType | undefined", "simplifiedType": "\"withoutResponse\" | \"onlyResponse\" | \"onlyData\" | \"fetchApi\" | \"all\" | null", "required": false, "deprecated": false }, { "name": "throwOnError", "description": "Controls whether errors are thrown as exceptions or returned in the result.\n\nCan be a boolean or a function that receives the error and decides whether to throw.\nWhen true, errors are thrown as exceptions instead of being returned in the result object.", "tags": [ { "name": "default", "text": "false" }, { "name": "example", "text": "```ts\n// Always throw errors\nthrowOnError: true\ntry {\n const data = await callApi(\"/users\");\n console.log(\"Users:\", data);\n} catch (error) {\n console.error(\"Request failed:\", error);\n}\n\n// Never throw errors (default)\nthrowOnError: false\nconst { data, error } = await callApi(\"/users\");\nif (error) {\n console.error(\"Request failed:\", error);\n}\n\n// Conditionally throw based on error type\nthrowOnError: (error) => {\n // Throw on client errors (4xx) but not server errors (5xx)\n return error.response?.status >= 400 && error.response?.status < 500;\n}\n\n// Throw only on specific status codes\nthrowOnError: (error) => {\n const criticalErrors = [401, 403, 404];\n return criticalErrors.includes(error.response?.status);\n}\n\n// Throw on validation errors but not network errors\nthrowOnError: (error) => {\n return error.type === \"validation\";\n}\n```" } ], "type": "ThrowOnErrorType | undefined", "simplifiedType": "function | boolean", "required": false, "deprecated": false }, { "name": "baseURL", "description": "Base URL for all API requests. Will only be prepended to relative URLs.\n\nAbsolute URLs (starting with http/https) will not be prepended by the baseURL.", "tags": [ { "name": "example", "text": "```ts\n// Set base URL for all requests\nbaseURL: \"https://api.example.com/v1\"\n\n// Then use relative URLs in requests\ncallApi(\"/users\") // → https://api.example.com/v1/users\ncallApi(\"/posts/123\") // → https://api.example.com/v1/posts/123\n\n// Environment-specific base URLs\nbaseURL: process.env.NODE_ENV === \"production\"\n ? \"https://api.example.com\"\n : \"http://localhost:3000/api\"\n```" } ], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "fullURL", "description": "Resolved request URL after processing baseURL, parameters, and query strings (readonly)\n\nThis is the final URL that will be used for the HTTP request, computed from\nbaseURL, initURL, params, and query parameters.", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "initURLNormalized", "description": "The URL string after normalization, with method modifiers removed(readonly)\n\nMethod modifiers like \"@get/\", \"@post/\" are stripped to create a clean URL\nfor parameter substitution and final URL construction.", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "params", "description": "Parameters to be substituted into URL path segments.\n\nSupports both object-style (named parameters) and array-style (positional parameters)\nfor flexible URL parameter substitution.", "tags": [ { "name": "example", "text": "```typescript\n// Object-style parameters (recommended)\nconst namedParams: URLOptions = {\n initURL: \"/users/:userId/posts/:postId\",\n params: { userId: \"123\", postId: \"456\" }\n};\n// Results in: /users/123/posts/456\n\n// Array-style parameters (positional)\nconst positionalParams: URLOptions = {\n initURL: \"/users/:userId/posts/:postId\",\n params: [\"123\", \"456\"] // Maps in order: userId=123, postId=456\n};\n// Results in: /users/123/posts/456\n\n// Single parameter\nconst singleParam: URLOptions = {\n initURL: \"/users/:id\",\n params: { id: \"user-123\" }\n};\n// Results in: /users/user-123\n```" } ], "type": "Record | (string | number | boolean)[] | undefined", "simplifiedType": "array | Record", "required": false, "deprecated": false }, { "name": "query", "description": "Query parameters to append to the URL as search parameters.\n\nThese will be serialized into the URL query string using standard\nURL encoding practices.", "tags": [ { "name": "example", "text": "```typescript\n// Basic query parameters\nconst queryOptions: URLOptions = {\n initURL: \"/users\",\n query: {\n page: 1,\n limit: 10,\n search: \"john doe\",\n active: true\n }\n};\n// Results in: /users?page=1&limit=10&search=john%20doe&active=true\n\n// Filtering and sorting\nconst filterOptions: URLOptions = {\n initURL: \"/products\",\n query: {\n category: \"electronics\",\n minPrice: 100,\n maxPrice: 500,\n sortBy: \"price\",\n order: \"asc\"\n }\n};\n// Results in: /products?category=electronics&minPrice=100&maxPrice=500&sortBy=price&order=asc\n```" } ], "type": "Record | URLSearchParams | undefined", "simplifiedType": "object | Record", "required": false, "deprecated": false }, { "name": "auth", "description": "Automatically add an Authorization header value.\n\nSupports multiple authentication patterns:\n- String: Direct authorization header value\n- Auth object: Structured authentication configuration\n\n```", "tags": [], "type": "AuthOption", "simplifiedType": "AuthOption", "required": false, "deprecated": false }, { "name": "bodySerializer", "description": "Custom function to serialize request body objects into strings.\n\nUseful for custom serialization formats or when the default JSON\nserialization doesn't meet your needs.", "tags": [ { "name": "example", "text": "```ts\n// Custom form data serialization\nbodySerializer: (data) => {\n const formData = new FormData();\n Object.entries(data).forEach(([key, value]) => {\n formData.append(key, String(value));\n });\n return formData.toString();\n}\n\n// XML serialization\nbodySerializer: (data) => {\n return `${Object.entries(data)\n .map(([key, value]) => `<${key}>${value}`)\n .join('')}`;\n}\n\n// Custom JSON with specific formatting\nbodySerializer: (data) => JSON.stringify(data, null, 2)\n```" } ], "type": "((bodyData: Record) => string) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "cloneResponse", "description": "Whether to clone the response so it can be read multiple times.\n\nBy default, response streams can only be consumed once. Enable this when you need\nto read the response in multiple places (e.g., in hooks and main code).", "tags": [ { "name": "see", "text": "https://developer.mozilla.org/en-US/docs/Web/API/Response/clone" }, { "name": "default", "text": "false" } ], "type": "boolean | undefined", "simplifiedType": "boolean", "required": false, "deprecated": false }, { "name": "customFetchImpl", "description": "Custom fetch implementation to replace the default fetch function.\n\nUseful for testing, adding custom behavior, or using alternative HTTP clients\nthat implement the fetch API interface.", "tags": [ { "name": "example", "text": "```ts\n// Use node-fetch in Node.js environments\nimport fetch from 'node-fetch';\n\n// Mock fetch for testing\ncustomFetchImpl: async (url, init) => {\n return new Response(JSON.stringify({ mocked: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n });\n}\n\n// Add custom logging to all requests\ncustomFetchImpl: async (url, init) => {\n console.log(`Fetching: ${url}`);\n const response = await fetch(url, init);\n console.log(`Response: ${response.status}`);\n return response;\n}\n\n// Use with custom HTTP client\ncustomFetchImpl: async (url, init) => {\n // Convert to your preferred HTTP client format\n return await customHttpClient.request({\n url: url.toString(),\n method: init?.method || 'GET',\n headers: init?.headers,\n body: init?.body\n });\n}\n```" } ], "type": "((input: string | Request | URL, init?: RequestInit) => Promise) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "defaultHTTPErrorMessage", "description": "Default HTTP error message when server doesn't provide one.\n\nCan be a static string or a function that receives error context\nto generate dynamic error messages based on the response.", "tags": [ { "name": "default", "text": "\"Failed to fetch data from server!\"" }, { "name": "example", "text": "```ts\n// Static error message\ndefaultHTTPErrorMessage: \"API request failed. Please try again.\"\n\n// Dynamic error message based on status code\ndefaultHTTPErrorMessage: ({ response }) => {\n switch (response.status) {\n case 401: return \"Authentication required. Please log in.\";\n case 403: return \"Access denied. Insufficient permissions.\";\n case 404: return \"Resource not found.\";\n case 429: return \"Too many requests. Please wait and try again.\";\n case 500: return \"Server error. Please contact support.\";\n default: return `Request failed with status ${response.status}`;\n }\n}\n\n// Include error data in message\ndefaultHTTPErrorMessage: ({ errorData, response }) => {\n const userMessage = errorData?.message || \"Unknown error occurred\";\n return `${userMessage} (Status: ${response.status})`;\n}\n```" } ], "type": "string | ((context: Pick, \"response\" | \"errorData\">) => string) | undefined", "simplifiedType": "function | string", "required": false, "deprecated": false }, { "name": "forcefullyCalculateRequestStreamSize", "description": "Forces calculation of total byte size from request body streams.\n\nUseful when the Content-Length header is missing or incorrect, and you need\naccurate size information for progress tracking.", "tags": [ { "name": "default", "text": "false" } ], "type": "boolean | undefined", "simplifiedType": "boolean", "required": false, "deprecated": false }, { "name": "meta", "description": "Optional metadata field for associating additional information with requests.\n\nUseful for logging, tracing, or handling specific cases in shared interceptors.\nThe meta object is passed through to all hooks and can be accessed in error handlers.", "tags": [ { "name": "example", "text": "```ts\nconst callMainApi = callApi.create({\n\tbaseURL: \"https://main-api.com\",\n\tonResponseError: ({ response, options }) => {\n\t\tif (options.meta?.userId) {\n\t\t\tconsole.error(`User ${options.meta.userId} made an error`);\n\t\t}\n\t},\n});\n\nconst response = await callMainApi({\n\turl: \"https://example.com/api/data\",\n\tmeta: { userId: \"123\" },\n});\n\n// Use case: Request tracking\nconst result = await callMainApi({\n url: \"https://example.com/api/data\",\n meta: {\n requestId: generateId(),\n source: \"user-dashboard\",\n priority: \"high\"\n }\n});\n\n// Use case: Feature flags\nconst client = callApi.create({\n baseURL: \"https://api.example.com\",\n meta: {\n features: [\"newUI\", \"betaFeature\"],\n experiment: \"variantA\"\n }\n});\n```" } ], "type": "DefaultMetaObject | undefined", "simplifiedType": "DefaultMetaObject", "required": false, "deprecated": false }, { "name": "responseParser", "description": "Custom function to parse response strings into actual value instead of the default response.json().\n\nUseful when you need custom parsing logic for specific response formats.", "tags": [ { "name": "example", "text": "```ts\nresponseParser: (text) => {\n return JSON.parse(text);\n}\n\n// Parse XML responses\nresponseParser: (text) => {\n const parser = new DOMParser();\n const doc = parser.parseFromString(text, \"text/xml\");\n return xmlToObject(doc);\n}\n\n// Parse CSV responses\nresponseParser: (text) => {\n const lines = text.split('\\n');\n const headers = lines[0].split(',');\n const data = lines.slice(1).map(line => {\n const values = line.split(',');\n return headers.reduce((obj, header, index) => {\n obj[header] = values[index];\n return obj;\n }, {});\n });\n return data;\n}\n\n```" } ], "type": "ResponseParser | undefined", "simplifiedType": "ResponseParser", "required": false, "deprecated": false }, { "name": "responseType", "description": "Expected response type, determines how the response body is parsed.\n\nDifferent response types trigger different parsing methods:\n- **\"json\"**: Parses as JSON using response.json()\n- **\"text\"**: Returns as plain text using response.text()\n- **\"blob\"**: Returns as Blob using response.blob()\n- **\"arrayBuffer\"**: Returns as ArrayBuffer using response.arrayBuffer()\n- **\"stream\"**: Returns the response body stream directly", "tags": [ { "name": "default", "text": "\"json\"" }, { "name": "example", "text": "```ts\n// JSON API responses (default)\nresponseType: \"json\"\n\n// Plain text responses\nresponseType: \"text\"\n// Usage: const csvData = await callApi(\"/export.csv\", { responseType: \"text\" });\n\n// File downloads\nresponseType: \"blob\"\n// Usage: const file = await callApi(\"/download/file.pdf\", { responseType: \"blob\" });\n\n// Binary data\nresponseType: \"arrayBuffer\"\n// Usage: const buffer = await callApi(\"/binary-data\", { responseType: \"arrayBuffer\" });\n\n// Streaming responses\nresponseType: \"stream\"\n// Usage: const stream = await callApi(\"/large-dataset\", { responseType: \"stream\" });\n```" } ], "type": "ResponseTypeType | undefined", "simplifiedType": "\"text\" | \"stream\" | \"json\" | \"formData\" | \"blob\" | \"arrayBuffer\" | null", "required": false, "deprecated": false }, { "name": "plugins", "description": "Array of base CallApi plugins to extend library functionality.\n\nBase plugins are applied to all instances created from this base configuration\nand provide foundational functionality like authentication, logging, or caching.", "tags": [ { "name": "example", "text": "```ts\n// Add logging plugin\n\n// Create base client with common plugins\nconst callApi = createFetchClient({\n baseURL: \"https://api.example.com\",\n plugins: [loggerPlugin({ enabled: true })]\n});\n\n// All requests inherit base plugins\nawait callApi(\"/users\");\nawait callApi(\"/posts\");\n\n```" } ], "type": "DefaultPluginArray | undefined", "simplifiedType": "DefaultPluginArray", "required": false, "deprecated": false }, { "name": "schema", "description": "Base validation schemas for the client configuration.\n\nDefines validation rules for requests and responses that apply to all\ninstances created from this base configuration. Provides type safety\nand runtime validation for API interactions.", "tags": [], "type": "BaseCallApiSchemaAndConfig | undefined", "simplifiedType": "BaseCallApiSchemaAndConfig", "required": false, "deprecated": false }, { "name": "skipAutoMergeFor", "description": "Controls which configuration parts skip automatic merging between base and instance configs.\n\nBy default, CallApi automatically merges base configuration with instance configuration.\nThis option allows you to disable automatic merging for specific parts when you need\nmanual control over how configurations are combined.", "tags": [ { "name": "enum", "text": "- **\"all\"**: Disables automatic merging for both request options and extra options\n- **\"options\"**: Disables automatic merging of extra options only (hooks, plugins, etc.)\n- **\"request\"**: Disables automatic merging of request options only (headers, body, etc.)" }, { "name": "example", "text": "```ts\n// Skip all automatic merging - full manual control\nconst client = callApi.create((ctx) => ({\n skipAutoMergeFor: \"all\",\n\n // Manually decide what to merge\n baseURL: ctx.options.baseURL, // Keep base URL\n timeout: 5000, // Override timeout\n headers: {\n ...ctx.request.headers, // Merge headers manually\n \"X-Custom\": \"value\" // Add custom header\n }\n}));\n\n// Skip options merging - manual plugin/hook control\nconst client = callApi.create((ctx) => ({\n skipAutoMergeFor: \"options\",\n\n // Manually control which plugins to use\n plugins: [\n ...ctx.options.plugins?.filter(p => p.name !== \"unwanted\") || [],\n customPlugin\n ],\n\n // Request options still auto-merge\n method: \"POST\"\n}));\n\n// Skip request merging - manual request control\nconst client = callApi.create((ctx) => ({\n skipAutoMergeFor: \"request\",\n\n // Extra options still auto-merge (plugins, hooks, etc.)\n\n // Manually control request options\n headers: {\n \"Content-Type\": \"application/json\",\n // Don't merge base headers\n },\n method: ctx.request.method || \"GET\"\n}));\n\n// Use case: Conditional merging based on request\nconst client = createFetchClient((ctx) => ({\n skipAutoMergeFor: \"options\",\n\n // Only use auth plugin for protected routes\n plugins: ctx.initURL.includes(\"/protected/\")\n ? [...(ctx.options.plugins || []), authPlugin]\n : ctx.options.plugins?.filter(p => p.name !== \"auth\") || []\n}));\n```" } ], "type": "\"all\" | \"request\" | \"options\" | undefined", "simplifiedType": "\"options\" | \"request\" | \"all\"", "required": false, "deprecated": false } ] }} /> # CallApi: Comparisons URL: /docs/comparisons Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/comparisons.mdx How is CallApi different from other existing fetching libraries? CallApi is a modern fetch wrapper designed to solve real problems. Here's how it compares to other popular libraries. Philosophy [#philosophy] CallApi is built on four core principles: **Lightweight:** Under 6KB. Zero dependencies. Pure ESM. **Simple:** Based on Fetch API, mirrors its interface 1-to-1. Only adds useful features with sensible defaults. **Type-safe:** Full TypeScript support with type inference via schemas, validators, and manual generics. **Extensible:** Plugins and hooks let you add or modify features without changing core code. CallApi vs Axios [#callapi-vs-axios] Overview [#overview] Axios has been fundamental to web development for years. However, modern web development has evolved, and CallApi addresses many of Axios's limitations. Key Differences [#key-differences] **Axios:** * Based on XMLHttpRequest (legacy API) * Custom API design * Verbose configuration for some use cases * Requires adapters for different environments **CallApi:** * Based on modern Fetch API * Drop-in replacement for fetch * Concise, intuitive API * Works everywhere (browsers, Node 18+, Deno, Bun, Workers) **Features Comparison:** | Feature | Axios | CallApi | | ----------------------------- | ----- | --------------------------- | | Request/Response Interceptors | ✅ | ✅ (Hooks & Plugins) | | Automatic JSON Parsing | ✅ | ✅ (Content-Type aware) | | Timeout Support | ✅ | ✅ | | Request Cancellation | ✅ | ✅ (Built-in) | | Progress Events | ✅ | ✅ (Streaming) | | Retry Logic | ❌ | ✅ (Exponential backoff) | | Request Deduplication | ❌ | ✅ | | Schema Validation | ❌ | ✅ (Any Standard Schema) | | TypeScript Type Inference | ❌ | ✅ | | Plugin System | ❌ | ✅ | | URL Helpers | ❌ | ✅ (Params, query, prefixes) | **Size Comparison:** * **Axios**: \~13KB minified + gzipped * **CallApi**: \<6KB minified + gzipped CallApi is over 50% smaller while providing more features. Migration Example [#migration-example] ```ts import axios from "axios"; const api = axios.create({ baseURL: "https://api.example.com", timeout: 10000, headers: { "Content-Type": "application/json", }, }); // Add auth token api.interceptors.request.use((config) => { config.headers.Authorization = `Bearer ${getToken()}`; return config; }); // Handle errors api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { redirectToLogin(); } return Promise.reject(error); } ); try { const response = await api.get("/users"); const users = response.data; } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response?.data); } } ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", timeout: 10000, // Add auth token onRequest: ({ request }) => { request.headers.set("Authorization", `Bearer ${getToken()}`); }, // Handle errors onError: ({ error, response }) => { if (response?.status === 401) { redirectToLogin(); } }, }); const { data: users, error } = await api("/users"); if (error) { console.error(error.errorData); } ``` When to Choose Axios [#when-to-choose-axios] * You need IE11 support * Your team is deeply familiar with Axios and migration cost is high * You rely on Axios-specific ecosystem packages When to Choose CallApi [#when-to-choose-callapi] * You want modern, fetch-based API * Bundle size matters * You need TypeScript type inference * You want built-in retries and deduplication * You need schema validation * You're starting a new project CallApi vs Ky [#callapi-vs-ky] Overview [#overview-1] Ky is a popular modern fetch wrapper with a focus on simplicity and developer experience. Key Differences [#key-differences-1] | Feature | Ky | CallApi | | --------------- | ----- | ---------------------- | | Fetch-based | ✅ | ✅ | | Timeout Support | ✅ | ✅ | | Retry Logic | ✅ | ✅ (More flexible) | | Hooks | ✅ | ✅ (More comprehensive) | | JSON Handling | ✅ | ✅ (Content-Type aware) | | Bundle Size | \~5KB | \<6KB | | Feature | Ky | CallApi | | --------------------- | ---------- | ---------------- | | Request Deduplication | ❌ | ✅ (3 strategies) | | Schema Validation | ❌ | ✅ | | Type Inference | ⚠️ Limited | ✅ Full | | Plugin System | ❌ | ✅ | | Streaming Progress | ❌ | ✅ | | URL Helpers | ❌ | ✅ | | Method Prefixes | ❌ | ✅ | **Ky:** ```ts import ky from "ky"; type User = { id: number; name: string }; const user = await ky.get("api/users/1").json(); ``` **CallApi:** ```ts import { callApi } from "@zayne-labs/callapi"; import { z } from "zod"; const userSchema = z.object({ id: z.number(), name: z.string(), }); const { data: user } = await callApi("api/users/1", { schema: { data: userSchema }, // Type automatically inferred! }); ``` When to Choose Ky [#when-to-choose-ky] * You want a minimal API with no extra features * You don't need schema validation or type inference * You prefer chaining API style When to Choose CallApi [#when-to-choose-callapi-1] * You need request deduplication * You want schema validation with type inference * You need a plugin system for extensibility * You want URL helpers and parameter substitution * You need streaming progress tracking CallApi vs Ofetch [#callapi-vs-ofetch] Overview [#overview-2] CallApi is highly inspired by Ofetch. Both share similar philosophies and design patterns. Key Differences [#key-differences-2] **Both libraries share:** * Fetch API foundation * Automatic JSON handling * Retry support with exponential backoff * Lifecycle hooks/interceptors * Object literal request body support * Timeout support * TypeScript-first design **CallApi adds:** | Feature | Ofetch | CallApi | | ----------------------- | ---------- | ----------------------------------- | | Request Deduplication | ❌ | ✅ (3 strategies) | | Schema Validation | ❌ | ✅ (Standard Schema) | | Type Inference | ⚠️ Limited | ✅ Full | | Plugin System | ❌ | ✅ | | Streaming Progress | ❌ | ✅ | | URL Params Substitution | ❌ | ✅ | | Method Prefixes | ❌ | ✅ (@get, @post, etc.) | | Result Modes | ❌ | ✅ (onlyData, onlyError, etc.) | | Structured Errors | ⚠️ Basic | ✅ Full (HTTPError, ValidationError) | **Size Comparison:** * **Ofetch**: \~8KB minified + gzipped * **CallApi**: \<6KB minified + gzipped CallApi is smaller while providing more features. Code Comparison [#code-comparison] **Ofetch:** ```ts import { ofetch } from "ofetch"; const api = ofetch.create({ baseURL: "https://api.example.com", onRequest: ({ options }) => { options.headers = { ...options.headers, Authorization: `Bearer ${token}`, }; }, }); const users = await api("/users"); ``` **CallApi:** ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", onRequest: ({ request }) => { request.headers.set("Authorization", `Bearer ${token}`); }, }); const { data: users } = await api("/users"); ``` **Ofetch:** ```ts import { ofetch } from "ofetch"; import { z } from "zod"; const userSchema = z.object({ id: z.number(), name: z.string(), }); // Manual validation required const data = await ofetch("/users/1"); const user = userSchema.parse(data); ``` **CallApi:** ```ts import { callApi } from "@zayne-labs/callapi"; import { z } from "zod"; const userSchema = z.object({ id: z.number(), name: z.string(), }); // Automatic validation + type inference const { data: user } = await callApi("/users/1", { schema: { data: userSchema }, }); // Type inferred as z.infer ``` **Ofetch:** ```ts // No built-in plugin system // Need to manually compose functionality ``` **CallApi:** ```ts import { createFetchClient } from "@zayne-labs/callapi"; import { definePlugin } from "@zayne-labs/callapi/utils"; const metricsPlugin = definePlugin({ id: "metrics", name: "Metrics Plugin", setup: ({ options }) => ({ options: { ...options, meta: { startTime: Date.now() }, }, }), hooks: { onSuccess: ({ options }) => { const duration = Date.now() - options.meta.startTime; console.log(`Request took ${duration}ms`); }, }, }); const api = createFetchClient({ plugins: [metricsPlugin], }); ``` When to Choose Ofetch [#when-to-choose-ofetch] * You need a proven, battle-tested library * You're already using it and don't need extra features * You prefer a slightly simpler API When to Choose CallApi [#when-to-choose-callapi-2] * You need request deduplication * You want built-in schema validation and type inference * You need a plugin system * You want streaming progress tracking * You need URL parameter substitution * You want method prefixes (@get, @post) * Smaller bundle size matters Feature Matrix [#feature-matrix] Comprehensive comparison of all libraries: | Feature | Axios | Ky | Ofetch | CallApi | | ------------------------ | ----- | --- | ------ | ------- | | **Core** | | | | | | Fetch-based | ❌ | ✅ | ✅ | ✅ | | Zero Dependencies | ❌ | ✅ | ✅ | ✅ | | Bundle Size | 13KB | 5KB | 8KB | \<6KB | | **Request Features** | | | | | | Timeout | ✅ | ✅ | ✅ | ✅ | | Retry Logic | ❌ | ✅ | ✅ | ✅ | | Exponential Backoff | ❌ | ⚠️ | ✅ | ✅ | | Request Cancellation | ✅ | ✅ | ✅ | ✅ | | Request Deduplication | ❌ | ❌ | ❌ | ✅ | | URL Param Substitution | ❌ | ❌ | ❌ | ✅ | | Method Prefixes | ❌ | ❌ | ❌ | ✅ | | **Response Features** | | | | | | Auto JSON Parsing | ✅ | ✅ | ✅ | ✅ | | Content-Type Detection | ⚠️ | ✅ | ✅ | ✅ | | Schema Validation | ❌ | ❌ | ❌ | ✅ | | Streaming Progress | ✅ | ❌ | ❌ | ✅ | | **Developer Experience** | | | | | | TypeScript Support | ⚠️ | ✅ | ✅ | ✅ | | Type Inference | ❌ | ⚠️ | ⚠️ | ✅ | | Hooks/Interceptors | ✅ | ✅ | ✅ | ✅ | | Plugin System | ❌ | ❌ | ❌ | ✅ | | Error Handling | ⚠️ | ✅ | ✅ | ✅ | ✅ = Full support | ⚠️ = Partial support | ❌ = Not supported Migration Paths [#migration-paths] For detailed migration guides from each library, see: * [Migration from Axios](/docs/migration-guide#from-axios) * [Migration from Ky](/docs/migration-guide#from-ky) * [Migration from Ofetch](/docs/migration-guide#from-ofetch) Summary [#summary] CallApi is designed for modern web development: **Choose CallApi if you want:** * Modern, fetch-based API * Built-in schema validation with type inference * Request deduplication for performance * Extensible plugin system * Comprehensive TypeScript support * Small bundle size (\<6KB) * All the convenience features you need **Choose other libraries if:** * **Axios**: You need IE11 support or have heavy Axios investment * **Ky**: You want absolute minimal API surface * **Ofetch**: You're already using it and satisfied CallApi provides the best balance of features, bundle size, and developer experience for modern applications. # CallApi: Error Handling URL: /docs/error-handling Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/error-handling.mdx Learn more about how to handle errors in CallApi It's prevalent knowledge that making network requests is inherently risky. Things can go wrong for many reasons: * The server might be down. * The server might respond with an error status like 404 Not Found (the resource doesn't exist) or 500 Internal Server Error (something broke on the server). * There might be a network issue. * The request might timeout. * The response data might not be in the format you expected (e.g., not valid JSON). When using the standard browser fetch API, handling these failures can sometimes be a bit clunky due to the following reasons: * Network errors throw one type of error * Non-2xx HTTP responses don't throw errors by default (you have to check response.ok) * Parsing errors might throw yet another type This can lead to complex if/else chains and unwieldy try...catch blocks just to figure out what went wrong. CallApi aims to make dealing with these failures much more predictable and convenient. Structure of the error property [#structure-of-the-error-property] As introduced in the [Getting Started](/docs/getting-started) guide, CallApi wraps responses in a result object with three key properties: `data`, `error`, and `response`. When something goes wrong, the `error` property contains a structured object with: 1. **name**: A string identifying the type of error (e.g., `'HTTPError'`, `'ValidationError'`, `'TypeError'`, `'TimeoutError'`, ...etc). 2. **message**: A brief description of what went wrong: * For HTTP errors: The error message from the server, or if not provided, falls back to the `defaultHTTPErrorMessage` option * For validation errors: A formatted error message derived from the validation issues array * For non-HTTP errors: The error message from the JavaScript error object that caused the error 3. **errorData**: The detailed error information: * For HTTP errors: It is set to the `parsed error response from the API` * For validation errors: It is set to the `validation issues array` * For non-HTTP errors: It is set to `false` 4. **originalError**: The original error object that caused the error: * For HTTP errors: `HTTPError` * For validation errors: `ValidationError` * For non-HTTP errors: The underlying javascript error object (e.g., `TypeError`, `DOMException`, etc.) ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; const { error } = await callApi("https://my-api.com/api/v1/session"); // @annotate: Hover over the error object to see the type ``` Handling HTTP Errors [#handling-http-errors] One of the most common types of errors you'll encounter is when the server responds with a status code outside the 200-299 range (like 400, 401, 403, 404, 500, 503, etc.). Standard `fetch` doesn't throw an error for these responses. CallApi, by default, wraps these responses in an `HTTPError`. You can customize the error response data type by providing a second generic type argument to callApi. ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; type ErrorData = { errors: Array; message: string; }; const { error } = await callApi("/api/endpoint"); if (error) { console.log(error.errorData); } ``` Since the `error` property is a discriminated union, you can use the `isHTTPError` utility from `@zayne-labs/callapi/utils` to check if it's an HTTP error: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; import { isHTTPError } from "@zayne-labs/callapi/utils"; type UserData = { completed: boolean; id: number; title: string; userId: string; }; type ErrorResponseData = { errors?: Array; message?: string; }; const { data, error } = await callApi("https://my-api.com/api/v1/session"); if (isHTTPError(error)) { console.error(error); console.error(error.name); // 'HTTPError' console.error(error.message); console.error(error.errorData); // Will be set to the error response data } ``` Handling Validation Errors [#handling-validation-errors] When schema validation fails, CallApi wraps the failure in a `ValidationError`. See the [Validation](/docs/validation) section for details. You can use the `isValidationError` utility to check specifically for this error type: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; import { isValidationError } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const dataSchema = z.object({ id: z.number(), userId: z.string(), title: z.string(), completed: z.boolean(), }); const { data, error } = await callApi("https://my-api.com/api/v1/session", { schema: { data: dataSchema, }, }); if (isValidationError(error)) { console.error(error.name); // 'ValidationError' console.error(error.errorData); // Validation issues array } ``` Throwing Errors [#throwing-errors] Set `throwOnError: true` to throw errors instead of returning them—useful for libraries expecting promise rejection: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; import { isHTTPErrorInstance, isValidationErrorInstance } from "@zayne-labs/callapi/utils"; type UserData = { completed: boolean; id: number; title: string; userId: string; }; type ErrorResponseData = { errors?: Array; message?: string; }; try { const { data } = await callApi("https://my-api.com/api/v1/session", { throwOnError: true, }); } catch (error) { if (isHTTPErrorInstance(error)) { console.error(error.errorData); } if (isValidationErrorInstance(error)) { console.error(error.errorData); } } ``` **Conditional throwing:** You can also pass a function to `throwOnError` for conditional throwing based on the error context: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; import { isHTTPError, isValidationError } from "@zayne-labs/callapi/utils"; // Only throw for authentication errors const resultOne = await callApi("https://my-api.com/api/v1/session", { throwOnError: ({ response }) => response?.status === 401, }); // Throw for client errors (user mistakes) but not server errors (temporary issues) const resultTwo = await callApi("https://my-api.com/api/users", { throwOnError: ({ response }) => { if (!response) { return false; } return response.status >= 400 && response.status < 500; }, }); // Complex conditional logic based on error type and context const resultThree = await callApi("https://my-api.com/api/sensitive", { throwOnError: ({ error, response, options }) => { // Always throw validation errors - data integrity is critical if (isValidationError(error)) { return true; } // Throw HTTP errors for sensitive endpoints if (isHTTPError(error) && response?.status === 403 && options.initURL?.includes("/sensitive")) { return true; } // Throw rate limiting errors during business hours (handle differently off-hours) if (response?.status === 429) { const hour = new Date().getHours(); return hour >= 9 && hour <= 17; } // Return other errors in result object return false; }, }); ``` Type Narrowing [#type-narrowing] The `data` and `error` properties form a discriminated union—if one is present, the other is null. TypeScript automatically narrows types after error checks: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; import { isHTTPError } from "@zayne-labs/callapi/utils"; type UserData = { completed: boolean; id: number; title: string; userId: string; }; type ErrorResponseData = { errors?: Array; message?: string; }; const { data, error } = await callApi("https://my-api.com/api/v1/session"); if (isHTTPError(error)) { console.error(error); } else if (error) { console.error(error); } else { console.log(data); // TypeScript knows data is not null } ``` # CallApi: Extra Options URL: /docs/extra-options Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/extra-options.mdx Options that can be passed to a CallApi instance This page documents the configuration options specific to CallApi. {\n\t\tif (options.meta?.userId) {\n\t\t\tconsole.error(`User ${options.meta.userId} made an error`);\n\t\t}\n\t},\n});\n\nconst response = await callMainApi({\n\turl: \"https://example.com/api/data\",\n\tmeta: { userId: \"123\" },\n});\n\n// Use case: Request tracking\nconst result = await callMainApi({\n url: \"https://example.com/api/data\",\n meta: {\n requestId: generateId(),\n source: \"user-dashboard\",\n priority: \"high\"\n }\n});\n\n// Use case: Feature flags\nconst client = callApi.create({\n baseURL: \"https://api.example.com\",\n meta: {\n features: [\"newUI\", \"betaFeature\"],\n experiment: \"variantA\"\n }\n});\n```" } ], "type": "DefaultMetaObject | undefined", "simplifiedType": "DefaultMetaObject", "required": false, "deprecated": false }, { "name": "params", "description": "Parameters to be substituted into URL path segments.\n\nSupports both object-style (named parameters) and array-style (positional parameters)\nfor flexible URL parameter substitution.", "tags": [ { "name": "example", "text": "```typescript\n// Object-style parameters (recommended)\nconst namedParams: URLOptions = {\n initURL: \"/users/:userId/posts/:postId\",\n params: { userId: \"123\", postId: \"456\" }\n};\n// Results in: /users/123/posts/456\n\n// Array-style parameters (positional)\nconst positionalParams: URLOptions = {\n initURL: \"/users/:userId/posts/:postId\",\n params: [\"123\", \"456\"] // Maps in order: userId=123, postId=456\n};\n// Results in: /users/123/posts/456\n\n// Single parameter\nconst singleParam: URLOptions = {\n initURL: \"/users/:id\",\n params: { id: \"user-123\" }\n};\n// Results in: /users/user-123\n```" } ], "type": "Record | (string | number | boolean)[] | undefined", "simplifiedType": "array | Record", "required": false, "deprecated": false }, { "name": "query", "description": "Query parameters to append to the URL as search parameters.\n\nThese will be serialized into the URL query string using standard\nURL encoding practices.", "tags": [ { "name": "example", "text": "```typescript\n// Basic query parameters\nconst queryOptions: URLOptions = {\n initURL: \"/users\",\n query: {\n page: 1,\n limit: 10,\n search: \"john doe\",\n active: true\n }\n};\n// Results in: /users?page=1&limit=10&search=john%20doe&active=true\n\n// Filtering and sorting\nconst filterOptions: URLOptions = {\n initURL: \"/products\",\n query: {\n category: \"electronics\",\n minPrice: 100,\n maxPrice: 500,\n sortBy: \"price\",\n order: \"asc\"\n }\n};\n// Results in: /products?category=electronics&minPrice=100&maxPrice=500&sortBy=price&order=asc\n```" } ], "type": "URLSearchParams | Record | undefined", "simplifiedType": "Record | object", "required": false, "deprecated": false }, { "name": "onError", "description": "Hook called when any error occurs within the request/response lifecycle.\n\nThis is a unified error handler that catches both request errors (network failures,\ntimeouts, etc.) and response errors (HTTP error status codes). It's essentially\na combination of `onRequestError` and `onResponseError` hooks.", "tags": [ { "name": "param", "text": "context - Error context containing error details, request info, and response (if available)" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ErrorContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequest", "description": "Hook called before the HTTP request is sent and before any internal processing of the request object begins.\n\nThis is the ideal place to modify request headers, add authentication,\nimplement request logging, or perform any setup before the network call.", "tags": [ { "name": "param", "text": "context - Request context with mutable request object and configuration" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequestError", "description": "Hook called when an error occurs during the fetch request itself.\n\nThis handles network-level errors like connection failures, timeouts,\nDNS resolution errors, or other issues that prevent getting an HTTP response.\nNote that HTTP error status codes (4xx, 5xx) are handled by `onResponseError`.", "tags": [ { "name": "param", "text": "context - Request error context with error details and null response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestErrorContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequestReady", "description": "Hook called just before the HTTP request is sent and after the request has been processed.", "tags": [ { "name": "param", "text": "context - Request context with mutable request object and configuration" } ], "type": "((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRequestStream", "description": "Hook called during upload stream progress tracking.\n\nThis hook is triggered when uploading data (like file uploads) and provides\nprogress information about the upload. Useful for implementing progress bars\nor upload status indicators.", "tags": [ { "name": "param", "text": "context - Request stream context with progress event and request instance" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestStreamContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RequestStreamContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onResponse", "description": "Hook called when any HTTP response is received from the API.\n\nThis hook is triggered for both successful (2xx) and error (4xx, 5xx) responses.\nIt's useful for response logging, metrics collection, or any processing that\nshould happen regardless of response status.", "tags": [ { "name": "param", "text": "context - Response context with either success data or error information" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ResponseContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onResponseError", "description": "Hook called when an HTTP error response (4xx, 5xx) is received from the API.\n\nThis handles server-side errors where an HTTP response was successfully received\nbut indicates an error condition. Different from `onRequestError` which handles\nnetwork-level failures.", "tags": [ { "name": "param", "text": "context - Response error context with HTTP error details and response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseErrorContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ResponseErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onResponseStream", "description": "Hook called during download stream progress tracking.\n\nThis hook is triggered when downloading data (like file downloads) and provides\nprogress information about the download. Useful for implementing progress bars\nor download status indicators.", "tags": [ { "name": "param", "text": "context - Response stream context with progress event and response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseStreamContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ResponseStreamContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onRetry", "description": "Hook called when a request is being retried.\n\nThis hook is triggered before each retry attempt, providing information about\nthe previous failure and the current retry attempt number. Useful for implementing\ncustom retry logic, exponential backoff, or retry logging.", "tags": [ { "name": "param", "text": "context - Retry context with error details and retry attempt count" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RetryContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: RetryContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onSuccess", "description": "Hook called when a successful response (2xx status) is received from the API.\n\nThis hook is triggered only for successful responses and provides access to\nthe parsed response data. Ideal for success logging, caching, or post-processing\nof successful API responses.", "tags": [ { "name": "param", "text": "context - Success context with parsed response data and response object" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: SuccessContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: SuccessContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "onValidationError", "description": "Hook called when a validation error occurs.\n\nThis hook is triggered when request or response data fails validation against\na defined schema. It provides access to the validation error details and can\nbe used for custom error handling, logging, or fallback behavior.", "tags": [ { "name": "param", "text": "context - Validation error context with error details and response (if available)" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ValidationErrorContext; ResultMode: ResultModeType; }>>) => unknown) | (((context: ValidationErrorContext; ResultMode: ResultModeType; }>>) => unknown) | undefined)[] | undefined", "simplifiedType": "array | function", "required": false, "deprecated": false }, { "name": "dedupeCacheScope", "description": "Controls the scope of request deduplication caching.\n\n- `\"global\"`: Shares deduplication cache across all `createFetchClient` instances with the same `dedupeCacheScopeKey`.\n Useful for applications with multiple API clients that should share deduplication state.\n- `\"local\"`: Limits deduplication to requests within the same `createFetchClient` instance.\n Provides better isolation and is recommended for most use cases.\n\n\n**Real-world Scenarios:**\n- Use `\"global\"` when you have multiple API clients (user service, auth service, etc.) that might make overlapping requests\n- Use `\"local\"` (default) for single-purpose clients or when you want strict isolation between different parts of your app", "tags": [ { "name": "example", "text": "```ts\n// Local scope - each client has its own deduplication cache\nconst userClient = createFetchClient({ baseURL: \"/api/users\" });\nconst postClient = createFetchClient({ baseURL: \"/api/posts\" });\n// These clients won't share deduplication state\n\n// Global scope - share cache across related clients\nconst userClient = createFetchClient({\n baseURL: \"/api/users\",\n dedupeCacheScope: \"global\",\n});\nconst postClient = createFetchClient({\n baseURL: \"/api/posts\",\n dedupeCacheScope: \"global\",\n});\n// These clients will share deduplication state\n```" }, { "name": "default", "text": "\"local\"" } ], "type": "\"global\" | \"local\" | undefined", "simplifiedType": "\"local\" | \"global\"", "required": false, "deprecated": false }, { "name": "dedupeCacheScopeKey", "description": "Unique namespace for the global deduplication cache when using `dedupeCacheScope: \"global\"`.\n\nThis creates logical groupings of deduplication caches. All instances with the same key\nwill share the same cache namespace, allowing fine-grained control over which clients\nshare deduplication state.\n\n**Best Practices:**\n- Use descriptive names that reflect the logical grouping (e.g., \"user-service\", \"analytics-api\")\n- Keep scope keys consistent across related API clients\n- Consider using different scope keys for different environments (dev, staging, prod)\n- Avoid overly broad scope keys that might cause unintended cache sharing\n\n**Cache Management:**\n- Each scope key maintains its own independent cache\n- Caches are automatically cleaned up when no references remain\n- Consider the memory implications of multiple global scopes", "tags": [ { "name": "example", "text": "```ts\n// Group related API clients together\nconst userClient = createFetchClient({\n baseURL: \"/api/users\",\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: \"user-service\"\n});\nconst profileClient = createFetchClient({\n baseURL: \"/api/profiles\",\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: \"user-service\" // Same scope - will share cache\n});\n\n// Separate analytics client with its own cache\nconst analyticsClient = createFetchClient({\n baseURL: \"/api/analytics\",\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: \"analytics-service\" // Different scope\n});\n\n// Environment-specific scoping\nconst apiClient = createFetchClient({\n dedupeCacheScope: \"global\",\n dedupeCacheScopeKey: `api-${process.env.NODE_ENV}` // \"api-development\", \"api-production\", etc.\n});\n```" }, { "name": "default", "text": "\"default\"" } ], "type": "AnyString | \"default\" | ((context: RequestContext) => string | undefined) | undefined", "simplifiedType": "function | \"default\" | AnyString", "required": false, "deprecated": false }, { "name": "dedupeKey", "description": "Custom key generator for request deduplication.\n\nOverride the default key generation strategy to control exactly which requests\nare considered duplicates. The default key combines URL, method, body, and\nrelevant headers (excluding volatile ones like 'Date', 'Authorization', etc.).\n\n**Default Key Generation:**\nThe auto-generated key includes:\n- Full request URL (including query parameters)\n- HTTP method (GET, POST, etc.)\n- Request body (for POST/PUT/PATCH requests)\n- Stable headers (excludes Date, Authorization, User-Agent, etc.)\n\n**Custom Key Best Practices:**\n- Include only the parts of the request that should affect deduplication\n- Avoid including volatile data (timestamps, random IDs, etc.)\n- Consider performance - simpler keys are faster to compute and compare\n- Ensure keys are deterministic for the same logical request\n- Use consistent key formats across your application\n\n**Performance Considerations:**\n- Function-based keys are computed on every request - keep them lightweight\n- String keys are fastest but least flexible\n- Consider caching expensive key computations if needed", "tags": [ { "name": "example", "text": "```ts\nimport { callApi } from \"@zayne-labs/callapi\";\n\n// Simple static key - useful for singleton requests\nconst config = callApi(\"/api/config\", {\n dedupeKey: \"app-config\",\n dedupeStrategy: \"defer\" // Share the same config across all requests\n});\n\n// URL and method only - ignore headers and body\nconst userData = callApi(\"/api/user/123\", {\n dedupeKey: (context) => `${context.options.method}:${context.options.fullURL}`\n});\n\n// Include specific headers in deduplication\nconst apiCall = callApi(\"/api/data\", {\n dedupeKey: (context) => {\n const authHeader = context.request.headers.get(\"Authorization\");\n return `${context.options.fullURL}-${authHeader}`;\n }\n});\n\n// User-specific deduplication\nconst userSpecificCall = callApi(\"/api/dashboard\", {\n dedupeKey: (context) => {\n const userId = context.options.fullURL.match(/user\\/(\\d+)/)?.[1];\n return `dashboard-${userId}`;\n }\n});\n\n// Ignore certain query parameters\nconst searchCall = callApi(\"/api/search?q=test×tamp=123456\", {\n dedupeKey: (context) => {\n const url = new URL(context.options.fullURL);\n url.searchParams.delete(\"timestamp\"); // Remove volatile param\n return `search:${url.toString()}`;\n }\n});\n```" }, { "name": "default", "text": "Auto-generated from request details" } ], "type": "string | ((context: RequestContext) => string | undefined) | undefined", "simplifiedType": "function | string", "required": false, "deprecated": false }, { "name": "dedupeStrategy", "description": "Strategy for handling duplicate requests. Can be a static string or callback function.\n\n**Available Strategies:**\n- `\"cancel\"`: Cancel previous request when new one starts (good for search)\n- `\"defer\"`: Share response between duplicate requests (good for config loading)\n- `\"none\"`: No deduplication, all requests execute independently", "tags": [ { "name": "example", "text": "```ts\n// Static strategies\nconst searchClient = createFetchClient({\n dedupeStrategy: \"cancel\" // Cancel previous searches\n});\n\nconst configClient = createFetchClient({\n dedupeStrategy: \"defer\" // Share config across components\n});\n\n// Dynamic strategy based on request\nconst smartClient = createFetchClient({\n dedupeStrategy: (context) => {\n return context.options.method === \"GET\" ? \"defer\" : \"cancel\";\n }\n});\n\n// Search-as-you-type with cancel strategy\nconst handleSearch = async (query: string) => {\n try {\n const { data } = await callApi(\"/api/search\", {\n method: \"POST\",\n body: { query },\n dedupeStrategy: \"cancel\",\n dedupeKey: \"search\" // Cancel previous searches, only latest one goes through\n });\n\n updateSearchResults(data);\n } catch (error) {\n if (error.name === \"AbortError\") {\n // Previous search cancelled - (expected behavior)\n return;\n }\n console.error(\"Search failed:\", error);\n }\n};\n\n```" }, { "name": "default", "text": "\"cancel\"" } ], "type": "\"cancel\" | \"defer\" | \"none\" | ((context: RequestContext) => \"cancel\" | \"defer\" | \"none\") | undefined", "simplifiedType": "function | \"none\" | \"defer\" | \"cancel\"", "required": false, "deprecated": false }, { "name": "hooksExecutionMode", "description": "Controls the execution mode of all composed hooks (main + plugin hooks).\n\n- **\"parallel\"**: All hooks execute simultaneously via Promise.all() for better performance\n- **\"sequential\"**: All hooks execute one by one in registration order via await in a loop\n\nThis affects how ALL hooks execute together, regardless of their source (main or plugin).", "tags": [ { "name": "default", "text": "\"parallel\"" } ], "type": "\"parallel\" | \"sequential\" | undefined", "simplifiedType": "\"sequential\" | \"parallel\"", "required": false, "deprecated": false }, { "name": "fetchMiddleware", "description": "Wraps the fetch implementation to intercept requests at the network layer.\n\nTakes a context object containing the current fetch function and returns a new fetch function.\nUse it to cache responses, add logging, handle offline mode, or short-circuit requests etc.\nMultiple middleware compose in order: plugins → base config → per-request.\n\nUnlike `customFetchImpl`, middleware can call through to the original fetch.", "tags": [ { "name": "example", "text": "```ts\n// Cache responses\nconst cache = new Map();\n\nfetchMiddleware: (ctx) => async (input, init) => {\n const key = input.toString();\n\n const cachedResponse = cache.get(key);\n\n if (cachedResponse) {\n return cachedResponse.clone();\n }\n\n const response = await ctx.fetchImpl(input, init);\n cache.set(key, response.clone());\n\n return response;\n}\n\n// Handle offline\nfetchMiddleware: (ctx) => async (...parameters) => {\n if (!navigator.onLine) {\n return new Response('{\"error\": \"offline\"}', { status: 503 });\n }\n\n return ctx.fetchImpl(...parameters);\n}\n```" } ], "type": "((context: FetchMiddlewareContext; ResultMode: ResultModeType; }>>) => FetchImpl) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "resultMode", "description": "Dictates how CallApi processes and returns the final result\n\n- **\"all\"** (default): Returns `{ data, error, response }`. Standard lifecycle.\n- **\"onlyData\"**: Returns only the data from the response.\n- **\"onlyResponse\"**: Returns only the `Response` object.\n- **\"fetchApi\"**: Also returns only the `Response` object, but also skips parsing of the response body internally and data/errorData schema validation.\n- **\"withoutResponse\"**: Returns `{ data, error }`. Standard lifecycle, but omits the `response` property.\n\n\n**Note:**\nBy default, simplified modes (`\"onlyData\"`, `\"onlyResponse\"`, `\"fetchApi\"`) do not throw errors.\nSuccess/failure should be handled via hooks or by checking the return value (e.g., `if (data)` or `if (response?.ok)`).\nTo force an exception instead, set `throwOnError: true`.", "tags": [ { "name": "default", "text": "\"all\"" } ], "type": "ResultModeType | undefined", "simplifiedType": "\"withoutResponse\" | \"onlyResponse\" | \"onlyData\" | \"fetchApi\" | \"all\" | null", "required": false, "deprecated": false }, { "name": "retryAttempts", "description": "Number of allowed retry attempts on HTTP errors", "tags": [ { "name": "default", "text": "0" } ], "type": "number | undefined", "simplifiedType": "number", "required": false, "deprecated": false }, { "name": "retryCondition", "description": "Callback whose return value determines if a request should be retried or not", "tags": [], "type": "RetryCondition | undefined", "simplifiedType": "RetryCondition", "required": false, "deprecated": false }, { "name": "retryDelay", "description": "Delay between retries in milliseconds", "tags": [ { "name": "default", "text": "1000" } ], "type": "number | ((currentAttemptCount: number) => number) | undefined", "simplifiedType": "function | number", "required": false, "deprecated": false }, { "name": "retryMaxDelay", "description": "Maximum delay in milliseconds. Only applies to exponential strategy", "tags": [ { "name": "default", "text": "10000" } ], "type": "number | undefined", "simplifiedType": "number", "required": false, "deprecated": false }, { "name": "retryMethods", "description": "HTTP methods that are allowed to retry", "tags": [ { "name": "default", "text": "[\"GET\", \"POST\"]" } ], "type": "(AnyString | \"CONNECT\" | \"DELETE\" | \"GET\" | \"HEAD\" | \"OPTIONS\" | \"PATCH\" | \"POST\" | \"PUT\" | \"TRACE\")[] | undefined", "simplifiedType": "array", "required": false, "deprecated": false }, { "name": "retryStatusCodes", "description": "HTTP status codes that trigger a retry", "tags": [], "type": "(AnyNumber | 408 | 409 | 425 | 429 | 500 | 502 | 503 | 504)[] | undefined", "simplifiedType": "array", "required": false, "deprecated": false }, { "name": "retryStrategy", "description": "Strategy to use when retrying", "tags": [ { "name": "default", "text": "\"linear\"" } ], "type": "\"exponential\" | \"linear\" | undefined", "simplifiedType": "\"linear\" | \"exponential\"", "required": false, "deprecated": false }, { "name": "throwOnError", "description": "Controls whether errors are thrown as exceptions or returned in the result.\n\nCan be a boolean or a function that receives the error and decides whether to throw.\nWhen true, errors are thrown as exceptions instead of being returned in the result object.", "tags": [ { "name": "default", "text": "false" }, { "name": "example", "text": "```ts\n// Always throw errors\nthrowOnError: true\ntry {\n const data = await callApi(\"/users\");\n console.log(\"Users:\", data);\n} catch (error) {\n console.error(\"Request failed:\", error);\n}\n\n// Never throw errors (default)\nthrowOnError: false\nconst { data, error } = await callApi(\"/users\");\nif (error) {\n console.error(\"Request failed:\", error);\n}\n\n// Conditionally throw based on error type\nthrowOnError: (error) => {\n // Throw on client errors (4xx) but not server errors (5xx)\n return error.response?.status >= 400 && error.response?.status < 500;\n}\n\n// Throw only on specific status codes\nthrowOnError: (error) => {\n const criticalErrors = [401, 403, 404];\n return criticalErrors.includes(error.response?.status);\n}\n\n// Throw on validation errors but not network errors\nthrowOnError: (error) => {\n return error.type === \"validation\";\n}\n```" } ], "type": "ThrowOnErrorType | undefined", "simplifiedType": "function | boolean", "required": false, "deprecated": false }, { "name": "baseURL", "description": "Base URL for all API requests. Will only be prepended to relative URLs.\n\nAbsolute URLs (starting with http/https) will not be prepended by the baseURL.", "tags": [ { "name": "example", "text": "```ts\n// Set base URL for all requests\nbaseURL: \"https://api.example.com/v1\"\n\n// Then use relative URLs in requests\ncallApi(\"/users\") // → https://api.example.com/v1/users\ncallApi(\"/posts/123\") // → https://api.example.com/v1/posts/123\n\n// Environment-specific base URLs\nbaseURL: process.env.NODE_ENV === \"production\"\n ? \"https://api.example.com\"\n : \"http://localhost:3000/api\"\n```" } ], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "fullURL", "description": "Resolved request URL after processing baseURL, parameters, and query strings (readonly)\n\nThis is the final URL that will be used for the HTTP request, computed from\nbaseURL, initURL, params, and query parameters.", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "initURL", "description": "The original URL string passed to the callApi instance (readonly)\n\nThis preserves the original URL as provided, including any method modifiers like \"@get/\" or \"@post/\".", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "initURLNormalized", "description": "The URL string after normalization, with method modifiers removed(readonly)\n\nMethod modifiers like \"@get/\", \"@post/\" are stripped to create a clean URL\nfor parameter substitution and final URL construction.", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "bodySerializer", "description": "Custom function to serialize request body objects into strings.\n\nUseful for custom serialization formats or when the default JSON\nserialization doesn't meet your needs.", "tags": [ { "name": "example", "text": "```ts\n// Custom form data serialization\nbodySerializer: (data) => {\n const formData = new FormData();\n Object.entries(data).forEach(([key, value]) => {\n formData.append(key, String(value));\n });\n return formData.toString();\n}\n\n// XML serialization\nbodySerializer: (data) => {\n return `${Object.entries(data)\n .map(([key, value]) => `<${key}>${value}`)\n .join('')}`;\n}\n\n// Custom JSON with specific formatting\nbodySerializer: (data) => JSON.stringify(data, null, 2)\n```" } ], "type": "((bodyData: Record) => string) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "cloneResponse", "description": "Whether to clone the response so it can be read multiple times.\n\nBy default, response streams can only be consumed once. Enable this when you need\nto read the response in multiple places (e.g., in hooks and main code).", "tags": [ { "name": "see", "text": "https://developer.mozilla.org/en-US/docs/Web/API/Response/clone" }, { "name": "default", "text": "false" } ], "type": "boolean | undefined", "simplifiedType": "boolean", "required": false, "deprecated": false }, { "name": "customFetchImpl", "description": "Custom fetch implementation to replace the default fetch function.\n\nUseful for testing, adding custom behavior, or using alternative HTTP clients\nthat implement the fetch API interface.", "tags": [ { "name": "example", "text": "```ts\n// Use node-fetch in Node.js environments\nimport fetch from 'node-fetch';\n\n// Mock fetch for testing\ncustomFetchImpl: async (url, init) => {\n return new Response(JSON.stringify({ mocked: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n });\n}\n\n// Add custom logging to all requests\ncustomFetchImpl: async (url, init) => {\n console.log(`Fetching: ${url}`);\n const response = await fetch(url, init);\n console.log(`Response: ${response.status}`);\n return response;\n}\n\n// Use with custom HTTP client\ncustomFetchImpl: async (url, init) => {\n // Convert to your preferred HTTP client format\n return await customHttpClient.request({\n url: url.toString(),\n method: init?.method || 'GET',\n headers: init?.headers,\n body: init?.body\n });\n}\n```" } ], "type": "((input: string | Request | URL, init?: RequestInit) => Promise) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "defaultHTTPErrorMessage", "description": "Default HTTP error message when server doesn't provide one.\n\nCan be a static string or a function that receives error context\nto generate dynamic error messages based on the response.", "tags": [ { "name": "default", "text": "\"Failed to fetch data from server!\"" }, { "name": "example", "text": "```ts\n// Static error message\ndefaultHTTPErrorMessage: \"API request failed. Please try again.\"\n\n// Dynamic error message based on status code\ndefaultHTTPErrorMessage: ({ response }) => {\n switch (response.status) {\n case 401: return \"Authentication required. Please log in.\";\n case 403: return \"Access denied. Insufficient permissions.\";\n case 404: return \"Resource not found.\";\n case 429: return \"Too many requests. Please wait and try again.\";\n case 500: return \"Server error. Please contact support.\";\n default: return `Request failed with status ${response.status}`;\n }\n}\n\n// Include error data in message\ndefaultHTTPErrorMessage: ({ errorData, response }) => {\n const userMessage = errorData?.message || \"Unknown error occurred\";\n return `${userMessage} (Status: ${response.status})`;\n}\n```" } ], "type": "string | ((context: Pick, \"errorData\" | \"response\">) => string) | undefined", "simplifiedType": "function | string", "required": false, "deprecated": false }, { "name": "forcefullyCalculateRequestStreamSize", "description": "Forces calculation of total byte size from request body streams.\n\nUseful when the Content-Length header is missing or incorrect, and you need\naccurate size information for progress tracking.", "tags": [ { "name": "default", "text": "false" } ], "type": "boolean | undefined", "simplifiedType": "boolean", "required": false, "deprecated": false }, { "name": "responseParser", "description": "Custom function to parse response strings into actual value instead of the default response.json().\n\nUseful when you need custom parsing logic for specific response formats.", "tags": [ { "name": "example", "text": "```ts\nresponseParser: (text) => {\n return JSON.parse(text);\n}\n\n// Parse XML responses\nresponseParser: (text) => {\n const parser = new DOMParser();\n const doc = parser.parseFromString(text, \"text/xml\");\n return xmlToObject(doc);\n}\n\n// Parse CSV responses\nresponseParser: (text) => {\n const lines = text.split('\\n');\n const headers = lines[0].split(',');\n const data = lines.slice(1).map(line => {\n const values = line.split(',');\n return headers.reduce((obj, header, index) => {\n obj[header] = values[index];\n return obj;\n }, {});\n });\n return data;\n}\n\n```" } ], "type": "ResponseParser | undefined", "simplifiedType": "ResponseParser", "required": false, "deprecated": false }, { "name": "responseType", "description": "Expected response type, determines how the response body is parsed.\n\nDifferent response types trigger different parsing methods:\n- **\"json\"**: Parses as JSON using response.json()\n- **\"text\"**: Returns as plain text using response.text()\n- **\"blob\"**: Returns as Blob using response.blob()\n- **\"arrayBuffer\"**: Returns as ArrayBuffer using response.arrayBuffer()\n- **\"stream\"**: Returns the response body stream directly", "tags": [ { "name": "default", "text": "\"json\"" }, { "name": "example", "text": "```ts\n// JSON API responses (default)\nresponseType: \"json\"\n\n// Plain text responses\nresponseType: \"text\"\n// Usage: const csvData = await callApi(\"/export.csv\", { responseType: \"text\" });\n\n// File downloads\nresponseType: \"blob\"\n// Usage: const file = await callApi(\"/download/file.pdf\", { responseType: \"blob\" });\n\n// Binary data\nresponseType: \"arrayBuffer\"\n// Usage: const buffer = await callApi(\"/binary-data\", { responseType: \"arrayBuffer\" });\n\n// Streaming responses\nresponseType: \"stream\"\n// Usage: const stream = await callApi(\"/large-dataset\", { responseType: \"stream\" });\n```" } ], "type": "ResponseTypeType | undefined", "simplifiedType": "\"text\" | \"stream\" | \"json\" | \"formData\" | \"blob\" | \"arrayBuffer\" | null", "required": false, "deprecated": false }, { "name": "timeout", "description": "Request timeout in milliseconds. Request will be aborted if it takes longer.\n\nUseful for preventing requests from hanging indefinitely and providing\nbetter user experience with predictable response times.", "tags": [ { "name": "example", "text": "```ts\n// 5 second timeout\ntimeout: 5000\n\n// Different timeouts for different endpoints\nconst quickApi = createFetchClient({ timeout: 3000 }); // 3s for fast endpoints\nconst slowApi = createFetchClient({ timeout: 30000 }); // 30s for slow operations\n\n// Per-request timeout override\nawait callApi(\"/quick-data\", { timeout: 1000 });\nawait callApi(\"/slow-report\", { timeout: 60000 });\n\n// No timeout (use with caution)\ntimeout: 0\n```" } ], "type": "number | undefined", "simplifiedType": "number", "required": false, "deprecated": false }, { "name": "plugins", "description": "Array of instance-specific CallApi plugins or a function to configure plugins.\n\nInstance plugins are added to the base plugins and provide functionality\nspecific to this particular API instance. Can be a static array or a function\nthat receives base plugins and returns the instance plugins.", "tags": [], "type": "DefaultPluginArray | ((context: InferExtendPluginContext) => DefaultPluginArray) | undefined", "simplifiedType": "function | DefaultPluginArray", "required": false, "deprecated": false }, { "name": "schema", "description": "For instance-specific validation schemas\n\nDefines validation rules specific to this API instance, extending or overriding the base schema.\n\nCan be a static schema object or a function that receives base schema context and returns instance schemas.", "tags": [], "type": "CallApiSchema | ((context: InferExtendSchemaContext>, string>) => CallApiSchema) | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "schemaConfig", "description": "Instance-specific schema configuration or a function to configure schema behavior.\n\nControls how validation schemas are applied and behave for this specific API instance.\nCan override base schema configuration or extend it with instance-specific validation rules.", "tags": [], "type": "CallApiSchemaConfig | ((context: GetExtendSchemaConfigContext) => CallApiSchemaConfig) | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false } ] }} /> # CallApi: Getting Started URL: /docs/getting-started Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/getting-started.mdx How to get started with CallApi Installation [#installation] npm pnpm yarn bun ```bash npm install @zayne-labs/callapi ``` ```bash pnpm add @zayne-labs/callapi ``` ```bash yarn add @zayne-labs/callapi ``` ```bash bun add @zayne-labs/callapi ``` Or via CDN: ```html ``` Quick Start [#quick-start] To get started with callApi, simply import the function and make your first request: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; type Todo = { completed: boolean; id: number; title: string; userId: string; }; // @annotate: Hover over the data or error object to see the type const { data, error, response } = await callApi("https://jsonplaceholder.typicode.com/todos/1"); ``` As shown in the example, callApi returns a result object containing: * `data`: The response data * `error`: An error object containing info about any error that occurred during the lifecycle of the request * `response`: The Response object from the underlying fetch API You can specify the data type and error type via TypeScript generics, or using a [Validation Schema](/docs/getting-started#request-and-response-validation) to validate and infer the types automatically. ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; import { z } from "zod"; type Todo = { completed: boolean; id: number; title: string; userId: string; }; // Via TypeScript generics const { data, error, response } = await callApi("https://jsonplaceholder.typicode.com/todos/1"); // Via Validation Schemas (automatic type inference) const dataSchema = z.object({ userId: z.string(), id: z.number(), title: z.string(), completed: z.boolean(), }); const { data: validatedData } = await callApi("https://jsonplaceholder.typicode.com/todos/1", { schema: { data: dataSchema, }, }); // @annotate: Hover over any of the properties to see the type ``` The result object format can also be customized using the [`resultMode`](/docs/request-and-response-helpers#result-management) option. ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; type Todo = { completed: boolean; id: number; title: string; userId: string; }; // @annotate: Hover over the data to see the type const data = await callApi("https://jsonplaceholder.typicode.com/todos/1", { resultMode: "onlyData", }); ``` Creating a Configured Client [#creating-a-configured-client] Create a reusable client with base configuration using `createFetchClient`. Instance options override base defaults. ```ts twoslash title="callBackendApi.ts" import { createFetchClient } from "@zayne-labs/callapi"; export const callBackendApi = createFetchClient({ baseURL: "https://jsonplaceholder.typicode.com", retryAttempts: 3, credentials: "same-origin", timeout: 10000, }); type Todo = { completed: boolean; id: number; title: string; userId: string; }; const resultOne = await callBackendApi("/todos/10"); const resultTwo = await callBackendApi("/todos/5", { // Override timeout and retry attempts retryAttempts: 2, timeout: 5000, }); ``` Request and Response Validation [#request-and-response-validation] CallApi supports runtime validation using the [Standard Schema specification](https://github.com/standard-schema/standard-schema), compatible with Zod, Valibot, ArkType, and more. ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; import { z } from "zod"; const dataSchema = z.object({ id: z.number(), title: z.string(), completed: z.boolean(), }); const errorDataSchema = z.object({ message: z.string(), errors: z.array( z.object({ field: z.string(), message: z.string(), }) ), }); const { data, error } = await callApi("/todos/1", { schema: { data: dataSchema, // Validates successful response data errorData: errorDataSchema, // Validate error response data from the server }, }); ``` See the [Validation Guide](/docs/validation) for comprehensive details on validation strategies and best practices. Throwing Errors [#throwing-errors] You can throw errors instead of returning them by passing the `throwOnError` option. If you set the `throwOnError` option to `true`, the `callApi` function will throw the error. If set it to a function instead, it will be passed the error context object, and it should return a boolean indicating whether to throw the error or not. ```ts twoslash title="callBackendApi.ts" import { createFetchClient } from "@zayne-labs/callapi"; export const callBackendApi = createFetchClient({ baseURL: "https://jsonplaceholder.typicode.com", throwOnError: true, }); // @error: This will throw an error if the request fails or there is an error response const { data } = await callBackendApi<{ userId: number }>("https://jsonplaceholder.typicode.com/todos/1"); ``` Learn more about handling errors in the [Error Handling](/docs/error-handling) section. # CallApi: Hooks URL: /docs/hooks Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/hooks.mdx 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](/docs/middlewares) for details. You can configure hook execution using `hooksExecutionMode` (parallel vs sequential). Plugin hooks always execute before main hooks. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callApi = createFetchClient({ baseURL: "http://localhost:3000", onRequest: (ctx) => { // Do something with context object }, onRequestReady: (ctx) => { // Do something with context object }, onRequestError: (ctx) => { // Do something with context object }, onResponse: (ctx) => { // Do something with context object }, onResponseError: (ctx) => { // Do something with context object }, onValidationError: (ctx) => { // Do something with context object }, onError: (ctx) => { // Do something with context object }, onSuccess: (ctx) => { // Do something with context object }, onRetry: (ctx) => { // Do something with context object }, onRequestStream: (ctx) => { // Do something with context object }, onResponseStream: (ctx) => { // Do something with context object }, }); callApi("/api/data", { onRequest: (ctx) => {}, onRequestReady: (ctx) => {}, onRequestError: (ctx) => {}, onResponse: (ctx) => {}, onResponseError: (ctx) => {}, onValidationError: (ctx) => {}, onError: (ctx) => {}, onSuccess: (ctx) => {}, onRetry: (ctx) => {}, onRequestStream: (ctx) => {}, onResponseStream: (ctx) => {}, }); ``` What hooks are available and when do they run? [#what-hooks-are-available-and-when-do-they-run] Request Phase Hooks [#request-phase-hooks] onRequest [#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. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ onRequest: ({ request, options }) => { // Add auth header options.auth = localStorage.getItem("token"); // Add custom headers request.headers["X-Custom-ID"] = "123"; // Add environment header based on baseURL if (options.fullURL?.includes("api.dev")) { request.headers["X-Environment"] = "development"; } }, }); ``` onRequestReady [#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. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ onRequestReady: ({ request }) => { // Final check of the request object console.log("Final Headers:", request.headers); console.log("Final Body:", request.body); }, }); ``` onRequestStream [#onrequeststream] This hook is called during request body streaming, useful for tracking upload progress. ```ts title="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 [#onrequesterror] This hook is called when the request fails before reaching the server. You can use it to handle network errors, timeouts, etc. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ onRequestError: ({ error, request, options }) => { if (error.name === "TimeoutError") { console.error(`Request timeout: ${options.initURL}`); return; } console.error(`Network error: ${error.message}`); }, }); ``` Response Phase Hooks [#response-phase-hooks] onResponse [#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. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ onResponse: ({ data, error, request, response, options }) => { // Log all API calls console.log(`${request.method} ${options.initURL} - ${response?.status}`); // Handle specific status codes if (response?.status === 207) { console.warn("Partial success:", data); } }, }); ``` onResponseStream [#onresponsestream] This hook is called during response body streaming, perfect for tracking download progress. ```ts title="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 [#onsuccess] This hook is called only for successful responses. You can use it to handle successful responses, cache data, etc. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; type User = { email: string; id: string; name: string; }; const userCache = new Map(); const client = createFetchClient<{ Data: User[] }>({ onSuccess: ({ data, response, request, options }) => { // Cache user data data.forEach((user) => userCache.set(user.id, user)); }, }); ``` onResponseError [#onresponseerror] This hook is called for error responses (response.ok === false). You can use it to handle specific status codes, etc. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ onResponseError: ({ error, response, options }) => { switch (response.status) { 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 [#onerror] This hook is called for any error. It's basically a combination of onRequestError and onResponseError. It's perfect for global error handling. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ onError: ({ error, response, request, options }) => { // Send to error tracking // errorTracker.capture({ // type: error.name, // message: error.message, // url: request.url, // status: response?.status, // }); // Show user-friendly messages if (!response) { // showNetworkError(); } else if (response.status >= 500) { // showServerError(); } else if (response.status === 400) { // showValidationErrors(error.errorData); } }, }); ``` Retry Phase Hooks [#retry-phase-hooks] onRetry [#onretry] This hook is called before retrying a failed request. You can use it to handle stuff before retrying. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ // Advanced retry configuration retryAttempts: 3, retryStrategy: "exponential", retryStatusCodes: [408, 429, 500, 502, 503, 504], onRetry: ({ response }) => { // Handle stuff... }, }); ``` Validation Phase Hooks [#validation-phase-hooks] onValidationError [#onvalidationerror] This hook is called when request or response validation fails via the `schema` option. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ onValidationError: ({ error, response, request, options }) => { // Handle stuff... }, }); ``` Ways in which hooks can be provided [#ways-in-which-hooks-can-be-provided] Hooks can be provided at three levels: 1. **The Plugin Level**: (covered in [`plugins`](/docs/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. ```ts title="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 [#hook-execution-order] Hooks execute in the following order: **Plugin Hooks → Base Client Hooks → Instance Hooks** ```ts title="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 [#hook-configuration-options] hooksExecutionMode [#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 ```ts title="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 [#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](/docs/plugins) is often the better approach. ```ts title="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.2 Base, 2.3 Base, 3.2 Instance]` is then executed. Async Hooks [#async-hooks] All hooks can be async or return a Promise. When this is the case, the hook will be awaited internally: ```ts onRequest: async ({ request }) => { const token = await getAuthToken(); request.headers.Authorization = `Bearer ${token}`; }; ``` Type Safety [#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. ```ts twoslash title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; type User = { id: number; name: string; }; const client = createFetchClient<{ Data: User }>({ onSuccess: ({ data }) => { console.log(data.name); }, }); const { error } = await client("/api/data", { onSuccess: ({ data }) => { console.log(data.name); }, }); // @annotate: Hover over the data object to see the inferred type ``` Streaming [#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 [#types] ) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onRequest", "description": "Hook called before the HTTP request is sent and before any internal processing of the request object begins.\n\nThis is the ideal place to modify request headers, add authentication,\nimplement request logging, or perform any setup before the network call.", "tags": [ { "name": "param", "text": "context - Request context with mutable request object and configuration" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onRequestError", "description": "Hook called when an error occurs during the fetch request itself.\n\nThis handles network-level errors like connection failures, timeouts,\nDNS resolution errors, or other issues that prevent getting an HTTP response.\nNote that HTTP error status codes (4xx, 5xx) are handled by `onResponseError`.", "tags": [ { "name": "param", "text": "context - Request error context with error details and null response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestErrorContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onRequestReady", "description": "Hook called just before the HTTP request is sent and after the request has been processed.", "tags": [ { "name": "param", "text": "context - Request context with mutable request object and configuration" } ], "type": "((context: RequestContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onRequestStream", "description": "Hook called during upload stream progress tracking.\n\nThis hook is triggered when uploading data (like file uploads) and provides\nprogress information about the upload. Useful for implementing progress bars\nor upload status indicators.", "tags": [ { "name": "param", "text": "context - Request stream context with progress event and request instance" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RequestStreamContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onResponse", "description": "Hook called when any HTTP response is received from the API.\n\nThis hook is triggered for both successful (2xx) and error (4xx, 5xx) responses.\nIt's useful for response logging, metrics collection, or any processing that\nshould happen regardless of response status.", "tags": [ { "name": "param", "text": "context - Response context with either success data or error information" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onResponseError", "description": "Hook called when an HTTP error response (4xx, 5xx) is received from the API.\n\nThis handles server-side errors where an HTTP response was successfully received\nbut indicates an error condition. Different from `onRequestError` which handles\nnetwork-level failures.", "tags": [ { "name": "param", "text": "context - Response error context with HTTP error details and response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseErrorContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onResponseStream", "description": "Hook called during download stream progress tracking.\n\nThis hook is triggered when downloading data (like file downloads) and provides\nprogress information about the download. Useful for implementing progress bars\nor download status indicators.", "tags": [ { "name": "param", "text": "context - Response stream context with progress event and response" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ResponseStreamContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onRetry", "description": "Hook called when a request is being retried.\n\nThis hook is triggered before each retry attempt, providing information about\nthe previous failure and the current retry attempt number. Useful for implementing\ncustom retry logic, exponential backoff, or retry logging.", "tags": [ { "name": "param", "text": "context - Retry context with error details and retry attempt count" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: RetryContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onSuccess", "description": "Hook called when a successful response (2xx status) is received from the API.\n\nThis hook is triggered only for successful responses and provides access to\nthe parsed response data. Ideal for success logging, caching, or post-processing\nof successful API responses.", "tags": [ { "name": "param", "text": "context - Success context with parsed response data and response object" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: SuccessContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "onValidationError", "description": "Hook called when a validation error occurs.\n\nThis hook is triggered when request or response data fails validation against\na defined schema. It provides access to the validation error details and can\nbe used for custom error handling, logging, or fallback behavior.", "tags": [ { "name": "param", "text": "context - Validation error context with error details and response (if available)" }, { "name": "returns", "text": "Promise or void - Hook can be async or sync" } ], "type": "((context: ValidationErrorContext) => Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false } ] }} /> # CallApi: Introduction URL: /docs Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/index.mdx Understanding what CallApi is all about What is CallApi? [#what-is-callapi] CallApi is a modern [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) wrapper that adds essential features for real-world HTTP requests. If you know Fetch, you already know CallApi as it's a drop-in replacement with the same API, meaning you can use it anywhere Fetch works. Built-in features include request deduplication, automatic retries, structured error handling, lifecycle hooks, schema validation, and a plugin system. Works everywhere: browsers, Node.js 18+, Deno, Bun, and edge workers. Why CallApi? [#why-callapi] Most HTTP clients either lack critical features or ship with bloated bundles. CallApi addresses common pain points while staying lightweight: * **Modern standards**: Built on Fetch API, not legacy XMLHttpRequest * **Complete feature set**: Deduplication, retries, interceptors, validation—all included out of the box * **Intuitive API**: Simple, consistent interface without verbose configuration * **Full TypeScript support**: Automatic type inference from validation schemas * **Lightweight**: Less than 6KB minified and gzipped with zero dependencies * **Extensible**: Plugin system for custom functionality Features [#features] CallApi aims to be the most comprehensive as well as intuitive fetching library out there. It provides a wide range of features out of the box and still allows you to extend it with plugins. Here are some of the features: # CallApi: Middlewares URL: /docs/middlewares Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/middlewares.mdx Intercept and wrap internal operations in CallApi Middleware wraps and intercepts internal operations, giving you full control over execution flow. Currently CallApi supports fetch middleware for intercepting network requests at the lowest level, with more middleware types planned for future releases. **Middleware vs Hooks:** Middleware controls execution (can return custom responses, short-circuit requests). Hooks observe events (return values ignored). Use middleware for control flow, hooks for side effects. See [Hooks](/docs/hooks) for details. When a given middleware is defined at multiple levels (plugins, base config, per-request), they compose automatically in reverse order—the last middleware added wraps all previous ones. Fetch Middleware [#fetch-middleware] Fetch middleware wraps the underlying fetch implementation, giving you full control over when and how requests execute. Use it to cache responses, track progress, handle offline mode, add logging, or short-circuit requests. ```ts type FetchMiddleware = (context: RequestContext & { fetchImpl: FetchImpl }) => FetchImpl; ``` Each middleware receives a context object (containing the fetch implementation and request context) and returns a new fetch function. Using Fetch Middleware [#using-fetch-middleware] Base Config [#base-config] Apply fetch middleware to all requests by adding it to your client configuration: ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ baseURL: "https://api.example.com", fetchMiddleware: (ctx) => async (input, init) => { console.log("Request:", input); const response = await ctx.fetchImpl(input, init); console.log("Response:", response.status); return response; }, }); ``` Per-Request [#per-request] Add fetch middleware to individual requests for one-off modifications: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; const { data, error } = await callApi("/users", { fetchMiddleware: (ctx) => async (input, init) => { const start = Date.now(); const response = await ctx.fetchImpl(input, init); console.log(`Took ${Date.now() - start}ms`); return response; }, }); ``` In Plugins [#in-plugins] Plugins can define fetch middleware to add reusable functionality. See [Plugins](/docs/plugins) for details. ```ts twoslash title="plugins.ts" import { definePlugin } from "@zayne-labs/callapi/utils"; const loggingPlugin = definePlugin({ id: "logging", name: "Logging Plugin", middlewares: { fetchMiddleware: (ctx) => async (input, init) => { console.log("→", init?.method || "GET", input); const response = await ctx.fetchImpl(input, init); console.log("←", response.status, input); return response; }, }, }); ``` Fetch Middleware Composition [#fetch-middleware-composition] When multiple fetch middlewares are defined, they compose in order, with each wrapping the previous one like onion layers. The last middleware added gets the outermost position: **Execution flow: Instance → Base → Plugins (last to first) → customFetchImpl/fetch** ```ts title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ plugins: [cachingPlugin, loggingPlugin], fetchMiddleware: (ctx) => async (input, init) => { console.log("Base middleware"); return ctx.fetchImpl(input, init); }, }); await client("/users", { fetchMiddleware: (ctx) => async (input, init) => { console.log("Instance middleware"); return ctx.fetchImpl(input, init); }, }); // Execution order: Instance → Base → loggingPlugin → cachingPlugin → fetch ``` This reverse composition means the last middleware added gets the first chance to intercept the request. Examples [#examples] Response Caching [#response-caching] ```ts twoslash title="caching-plugin.ts" import { createFetchClient, type PluginSetupContext } from "@zayne-labs/callapi"; import { definePlugin } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const CacheOptionsSchema = z.object({ cacheLifetime: z.int().positive().optional(), cachePolicy: z.literal(["cache-first", "no-cache"]).optional(), }); export const cachingPlugin = () => { const cache = new Map(); return definePlugin({ id: "caching-plugin", name: "Caching Plugin", defineExtraOptions: () => CacheOptionsSchema, middlewares: ({ options, }: PluginSetupContext<{ InferredExtraOptions: typeof CacheOptionsSchema }>) => { const { cacheLifetime = 60_000, cachePolicy = "cache-first" } = options; return { fetchMiddleware: (ctx) => async (input, init) => { if (cachePolicy === "no-cache") { return ctx.fetchImpl(input, init); } const cacheKey = input instanceof Request ? input.url : input.toString(); const cachedEntry = cache.get(cacheKey); const fetchAndCache = async () => { const response = await ctx.fetchImpl(input, init); cache.set(cacheKey, { data: response.clone(), timestamp: Date.now() }); return response; }; if (!cachedEntry) { console.info(`[Caching Plugin] Cache miss: ${cacheKey}`); return fetchAndCache(); } const isCacheExpired = Date.now() - cachedEntry.timestamp > cacheLifetime; if (isCacheExpired) { console.info(`[Caching Plugin] Cache miss (expired): ${cacheKey}`); cache.delete(cacheKey); return fetchAndCache(); } console.info(`[Caching Plugin] Cache hit: ${cacheKey}`); return cachedEntry.data.clone(); }, }; }, }); }; const callBackendApi = createFetchClient({ baseURL: "https://api.example.com", plugins: [cachingPlugin()], cachePolicy: "cache-first", cacheLifetime: 2 * 60 * 1000, // 2 minutes }); await callBackendApi("/users"); await callBackendApi("/users/:id", { cachePolicy: "no-cache", // Skip cache for this request }); ``` Offline Detection [#offline-detection] ```ts title="offline.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ fetchMiddleware: (ctx) => async (input, init) => { if (!navigator.onLine) { return Response.json( { error: "No internet connection" }, { status: 503, headers: { "Content-Type": "application/json" }, } ); } return ctx.fetchImpl(input, init); }, }); ``` Request Timing [#request-timing] ```ts title="timing.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ fetchMiddleware: (ctx) => async (input, init) => { const start = performance.now(); try { const response = await ctx.fetchImpl(input, init); const duration = performance.now() - start; console.log(`${init?.method || "GET"} ${input} - ${duration.toFixed(2)}ms`); return response; } catch (error) { const duration = performance.now() - start; console.error(`${init?.method || "GET"} ${input} - Failed after ${duration.toFixed(2)}ms`); throw error; } }, }); ``` Error Handling in Middleware [#error-handling-in-middleware] Middleware can catch and handle errors or transform them before they reach your application: ```ts title="error-handling.ts" import { createFetchClient } from "@zayne-labs/callapi"; const client = createFetchClient({ fetchMiddleware: (ctx) => async (input, init) => { try { const response = await ctx.fetchImpl(input, init); if (response.status === 503) { return Response.json( { cached: true }, { status: 200, headers: { "Content-Type": "application/json" }, } ); } return response; } catch (error) { if (error instanceof TypeError && error.message.includes("fetch")) { return Response.json( { error: "Network error" }, { status: 0, headers: { "Content-Type": "application/json" }, } ); } throw error; } }, }); ``` Mock Responses for Testing [#mock-responses-for-testing] ```ts title="mock.ts" import { createFetchClient } from "@zayne-labs/callapi"; const mockData = { "/users/1": { id: 1, name: "John" }, "/users/2": { id: 2, name: "Jane" }, }; const client = createFetchClient({ fetchMiddleware: (ctx) => async (input, init) => { const url = input.toString(); // Return mock data without calling fetch if (url in mockData) { return Response.json(mockData[url as keyof typeof mockData], { status: 200, headers: { "Content-Type": "application/json" }, }); } // Fall through to real fetch for unmocked URLs return ctx.fetchImpl(input, init); }, }); ``` Middleware vs Hooks [#middleware-vs-hooks] **Use fetch middleware when you need to:** * Control whether fetch is called (short-circuit, return cached responses) * Replace the fetch implementation (use XHR, mock responses, alternative clients) * Transform the response (return a different Response object) * Wrap the network call (add retry logic, circuit breakers, request queuing) **Use hooks when you need to:** * Observe lifecycle events without controlling flow * Log or track metrics * Modify request options by mutation * Handle errors after they occur * Trigger side effects (update UI, analytics) **Key distinction:** Middleware wraps functions and controls what gets called and returned. Hooks receive context and can mutate objects, but their return values are ignored. # CallApi: Migration Guide URL: /docs/migration-guide Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/migration-guide.mdx Migrate to CallApi from other fetch libraries This guide helps you migrate from other popular fetch libraries to CallApi with step-by-step instructions and code examples. From Axios [#from-axios] Installation [#installation] First, install CallApi alongside or instead of Axios: ```bash pnpm install @zayne-labs/callapi # Optionally remove axios npm uninstall axios ``` Basic Request [#basic-request] ```ts import axios from "axios"; // GET request const responseOne = await axios.get("https://api.example.com/users"); const users = responseOne.data; // POST request const responseTwo = await axios.post("https://api.example.com/users", { name: "John Doe", email: "john@example.com", }); const user = responseTwo.data; ``` ```ts import { callApi } from "@zayne-labs/callapi"; // GET request const { data: users } = await callApi("https://api.example.com/users"); // POST request const { data: user } = await callApi("@post/https://api.example.com/users", { body: { name: "John Doe", email: "john@example.com", }, }); ``` Creating an Instance [#creating-an-instance] ```ts import axios from "axios"; const api = axios.create({ baseURL: "https://api.example.com", timeout: 10000, headers: { "Content-Type": "application/json", }, }); const response = await api.get("/users"); ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", timeout: 10000, headers: { "Content-Type": "application/json", }, }); const { data } = await api("/users"); ``` Interceptors → Hooks [#interceptors--hooks] ```ts import axios from "axios"; const api = axios.create({ baseURL: "https://api.example.com", }); // Request interceptor api.interceptors.request.use( (config) => { const token = localStorage.getItem("token"); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Response interceptor api.interceptors.response.use( (response) => { console.log("Response:", response.status); return response; }, (error) => { if (error.response?.status === 401) { // Redirect to login } return Promise.reject(error); } ); ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", // Request hook onRequest: ({ request }) => { const token = localStorage.getItem("token"); if (token) { request.headers.set("Authorization", `Bearer ${token}`); } }, // Success hook onSuccess: ({ response }) => { console.log("Response:", response.status); }, // Error hook onError: ({ response }) => { if (response?.status === 401) { // Redirect to login } }, }); ``` Error Handling [#error-handling] ```ts import axios from "axios"; try { const response = await axios.get("/users"); const users = response.data; } catch (error) { if (axios.isAxiosError(error)) { console.error("Status:", error.response?.status); console.error("Data:", error.response?.data); console.error("Message:", error.message); } else { console.error("Unexpected error:", error); } } ``` ```ts import { callApi } from "@zayne-labs/callapi"; import { isHTTPError } from "@zayne-labs/callapi/utils"; const { data: users, error } = await callApi("/users"); if (error) { if (isHTTPError(error)) { console.error("Status:", error.response?.status); console.error("Data:", error.errorData); console.error("Message:", error.message); } else { console.error("Unexpected error:", error.message); } } ``` Request Cancellation [#request-cancellation] ```ts import axios from "axios"; const controller = new AbortController(); try { const response = await axios.get("/users", { signal: controller.signal, }); } catch (error) { if (axios.isCancel(error)) { console.log("Request cancelled"); } } // Cancel the request controller.abort(); ``` ```ts import { callApi } from "@zayne-labs/callapi"; const controller = new AbortController(); const { data, error } = await callApi("/users", { signal: controller.signal, }); if (error?.name === "AbortError") { console.log("Request cancelled"); } // Cancel the request controller.abort(); ``` TypeScript Support [#typescript-support] ```ts import axios from "axios"; type User = { email: string; id: number; name: string; }; // Manual typing const response = await axios.get("/users/1"); const user = response.data; // Type: User // No runtime validation ``` ```ts import { callApi } from "@zayne-labs/callapi"; import { z } from "zod"; const userSchema = z.object({ id: z.number(), name: z.string(), email: z.string(), }); // Automatic type inference + runtime validation const { data: user } = await callApi("/users/1", { schema: { data: userSchema }, }); // Type automatically inferred from schema! ``` From Ky [#from-ky] Installation [#installation-1] `bash npm install @zayne-labs/callapi npm uninstall ky ` `bash pnpm add @zayne-labs/callapi pnpm remove ky ` `bash yarn add @zayne-labs/callapi yarn remove ky ` `bash bun add @zayne-labs/callapi bun remove ky ` Basic Usage [#basic-usage] ```ts import ky from "ky"; // GET request const users = await ky.get("https://api.example.com/users").json(); // POST request const user = await ky .post("https://api.example.com/users", { json: { name: "John Doe", email: "john@example.com", }, }) .json(); ``` ```ts import { callApi } from "@zayne-labs/callapi"; // GET request const { data: users } = await callApi("https://api.example.com/users"); // POST request const { data: user } = await callApi("@post/https://api.example.com/users", { body: { name: "John Doe", email: "john@example.com", }, }); ``` Creating an Instance [#creating-an-instance-1] ```ts import ky from "ky"; const api = ky.create({ prefixUrl: "https://api.example.com", timeout: 10000, retry: 2, }); const users = await api.get("users").json(); ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", timeout: 10000, retryAttempts: 2, }); const { data: users } = await api("/users"); ``` Hooks [#hooks] ```ts import ky from "ky"; const api = ky.create({ hooks: { beforeRequest: [ (request) => { request.headers.set("Authorization", `Bearer ${token}`); }, ], afterResponse: [ (request, options, response) => { console.log("Response:", response.status); }, ], }, }); ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ onRequest: ({ request }) => { request.headers.set("Authorization", `Bearer ${token}`); }, onSuccess: ({ response }) => { console.log("Response:", response.status); }, }); ``` Error Handling [#error-handling-1] ```ts import ky from "ky"; try { const users = await ky.get("api/users").json(); } catch (error) { if (error instanceof ky.HTTPError) { console.error("Status:", error.response.status); const errorData = await error.response.json(); console.error("Error data:", errorData); } } ``` ```ts import { callApi } from "@zayne-labs/callapi"; import { isHTTPError } from "@zayne-labs/callapi/utils"; const { data: users, error } = await callApi("api/users"); if (isHTTPError(error)) { console.error("Status:", error.response?.status); console.error("Error data:", error.errorData); // Already parsed! } ``` From Ofetch [#from-ofetch] Installation [#installation-2] `bash npm install @zayne-labs/callapi npm uninstall ofetch ` `bash pnpm add @zayne-labs/callapi pnpm remove ofetch ` `bash yarn add @zayne-labs/callapi yarn remove ofetch ` `bash bun add @zayne-labs/callapi bun remove ofetch ` Basic Usage [#basic-usage-1] ```ts import { ofetch } from "ofetch"; // GET request const users = await ofetch("https://api.example.com/users"); // POST request const user = await ofetch("https://api.example.com/users", { method: "POST", body: { name: "John Doe", email: "john@example.com", }, }); ``` ```ts import { callApi } from "@zayne-labs/callapi"; // GET request const { data: users } = await callApi("https://api.example.com/users"); // POST request const { data: user } = await callApi("@post/https://api.example.com/users", { body: { name: "John Doe", email: "john@example.com", }, }); ``` Creating an Instance [#creating-an-instance-2] ```ts import { ofetch } from "ofetch"; const api = ofetch.create({ baseURL: "https://api.example.com", retry: 2, retryDelay: 1000, }); const users = await api("/users"); ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", retryAttempts: 2, retryDelay: 1000, }); const { data: users } = await api("/users"); ``` Interceptors → Hooks [#interceptors--hooks-1] ```ts import { ofetch } from "ofetch"; const api = ofetch.create({ baseURL: "https://api.example.com", onRequest: ({ options }) => { options.headers = { ...options.headers, Authorization: `Bearer ${token}`, }; }, onResponse: ({ response }) => { console.log("Response:", response.status); }, onResponseError: ({ response }) => { console.error("Error:", response.status); }, }); ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", onRequest: ({ request }) => { request.headers.set("Authorization", `Bearer ${token}`); }, onSuccess: ({ response }) => { console.log("Response:", response.status); }, onError: ({ response }) => { console.error("Error:", response?.status); }, }); ``` Error Handling [#error-handling-2] ```ts import { ofetch } from "ofetch"; try { const users = await ofetch("/users"); } catch (error) { console.error("Error:", error.data); console.error("Status:", error.statusCode); } ``` ```ts import { callApi } from "@zayne-labs/callapi"; import { isHTTPError } from "@zayne-labs/callapi/utils"; const { data: users, error } = await callApi("/users"); if (isHTTPError(error)) { console.error("Error:", error.errorData); console.error("Status:", error.response?.status); } ``` Common Patterns [#common-patterns] Authentication [#authentication] ```ts // Various approaches depending on library // Usually involves interceptors or hooks ``` ```ts import { createFetchClient } from "@zayne-labs/callapi"; const api = createFetchClient({ baseURL: "https://api.example.com", onRequest: ({ request, options }) => { const token = localStorage.getItem("token"); if (token) { options.auth = token; } }, onError: async ({ error, response, options }) => { // Refresh token on 401 if (response?.status === 401) { const newToken = await refreshToken(); localStorage.setItem("token", newToken); // Retry request options.retryAttempts = 1; } }, }); ``` File Upload with Progress [#file-upload-with-progress] ```ts import axios from "axios"; const formData = new FormData(); formData.append("file", file); const response = await axios.post("/upload", formData, { onUploadProgress: (progressEvent) => { const progress = (progressEvent.loaded / progressEvent.total) * 100; console.log(`Upload progress: ${progress}%`); }, }); ``` ```ts import { callApi } from "@zayne-labs/callapi"; const formData = new FormData(); formData.append("file", file); const { data } = await callApi("@post/upload", { body: formData, onResponseStream: ({ event }) => { if (event.type === "progress") { console.log(`Upload progress: ${event.progress}%`); } }, }); ``` Query Parameters [#query-parameters] ```ts // Manual URL construction const responseOne = await api.get("/users?role=admin&status=active"); // Or using params option (varies by library) const responseTwo = await api.get("/users", { params: { role: "admin", status: "active" }, }); ``` ```ts import { callApi } from "@zayne-labs/callapi"; // Built-in query helper const { data: users } = await callApi("/users", { query: { role: "admin", status: "active" }, }); // Automatically constructs: /users?role=admin&status=active ``` URL Parameters [#url-parameters] ```ts // Manual URL construction const userId = 123; const response = await api.get(`/users/${userId}`); ``` ```ts import { callApi } from "@zayne-labs/callapi"; // Built-in param substitution const { data: user } = await callApi("/users/:id", { params: { id: 123 }, }); // Automatically constructs: /users/123 ``` Migration Checklist [#migration-checklist] * Install `@zayne-labs/callapi` * Update imports from `axios`/`ky`/`ofetch` to `callApi` * Replace `axios.create()` or `ky.create()` with `createFetchClient()` * Update HTTP method calls (`api.get()`, `api.post()`) to either `callApi('@method/path')` or use the `method` option. * Convert interceptors to hooks (`onRequest`, `onSuccess`, `onError`) * Update error handling to check the `error` property in the returned result object and use `isHTTPError(error)` * For React Query, see the [React Query Integration](/docs/integrations/react-query) guide. # CallApi: Plugins URL: /docs/plugins Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/plugins.mdx Extend CallApi's functionality with plugins Plugins extend CallApi with reusable functionality like authentication, logging, caching, or custom request/response handling. They can modify requests before they're sent, intercept fetch calls at the network layer, and hook into various points in the request lifecycle. Creating a Plugin [#creating-a-plugin] Use the `definePlugin` helper for type-safe plugin creation: ```ts twoslash title="plugins.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { definePlugin } from "@zayne-labs/callapi/utils"; const envPlugin = definePlugin({ id: "env-plugin", name: "Environment Plugin", description: "Adds environment-specific headers to requests", version: "1.0.0", setup: ({ request, options, initURL }) => { const env = process.env.NODE_ENV || "development"; const updatedRequest = { ...request, headers: { ...request.headers, "X-Environment": env }, }; const updatedOptions = { ...options, meta: { ...options.meta, env, }, }; const updatedInitURL = initURL.replace("http://localhost:3000", "http://localhost:3001"); return { initURL: updatedInitURL, options: updatedOptions, request: updatedRequest, }; }, }); const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", plugins: [envPlugin], }); ``` Or alternatively, you can also use TypeScript's `satisfies` keyword together with `CallApiPlugin` type to achieve the same effect: ```ts twoslash title="plugins.ts" import type { CallApiPlugin } from "@zayne-labs/callapi"; const envPlugin = { id: "env-plugin", name: "Environment Plugin", description: "Adds environment-specific headers to requests", version: "1.0.0", setup: ({ request }) => { const env = process.env.NODE_ENV || "development"; const updatedRequest = { ...request, headers: { ...request.headers, "X-Environment": env }, }; return { request: updatedRequest, }; }, } satisfies CallApiPlugin; ``` Using Plugins [#using-plugins] Base Plugins [#base-plugins] Add plugins when creating a client to apply them to all requests: ```ts title="api.ts" const callBackendApi = createFetchClient({ plugins: [ envPlugin, // Handle environment-specific configurations loggingPlugin, // Log request/response details ], }); ``` Per-Request Plugins [#per-request-plugins] Add plugins to individual requests for specific calls: ```ts title="api.ts" const { data } = await callBackendApi("/users", { plugins: [metricsPlugin], }); ``` By default, passing `plugins` to a request replaces base plugins. To keep base plugins and add new ones, use a callback. ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { envPlugin, loggingPlugin, metricsPlugin } from "./plugins"; const callBackendApi = createFetchClient({ plugins: [envPlugin, loggingPlugin], }); const { data } = await callBackendApi("/users", { plugins: ({ basePlugins }) => [...basePlugins, metricsPlugin], // Add metrics plugin while keeping base plugins }); ``` Plugin Anatomy [#plugin-anatomy] Setup Function [#setup-function] The `setup` function runs before any request processing begins. It receives the initial URL, options, and request, and can return modified versions of these values. This is useful for transforming requests before CallApi's internal processing. ```ts twoslash title="plugins.ts" import { definePlugin } from "@zayne-labs/callapi/utils"; const envPlugin = definePlugin({ id: "env-plugin", name: "Environment Plugin", description: "A plugin that adds environment-specific headers to requests", version: "1.0.0", setup: ({ request }) => { const env = process.env.NODE_ENV ?? "development"; const platform = globalThis.window !== undefined ? "browser" : "node"; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const updatedRequest = { ...request, headers: { ...request.headers, "X-Environment": env, "X-Client-Platform": platform, "X-Client-Timezone": timezone, }, }; return { request: updatedRequest, }; }, }); ``` Hooks [#hooks] Plugins can define hooks that run at different stages of the request lifecycle. Hooks can be an object or a function that returns an object (useful for accessing setup context). See [Hooks](/docs/hooks) for detailed information about available hooks. ```ts twoslash title="plugins.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { definePlugin } from "@zayne-labs/callapi/utils"; const myPlugin = definePlugin({ id: "my-plugin", name: "My Plugin", description: "A plugin that does something", version: "1.0.0", hooks: { onError: (ctx) => { // Do something with context object }, onSuccess: (ctx) => { // Do something with context object }, // More hooks can be added here }, }); const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", plugins: [myPlugin], }); ``` **Dynamic Hooks:** Hooks can also be a function that receives the plugin setup context: ```ts twoslash title="plugins.ts" import { definePlugin } from "@zayne-labs/callapi/utils"; const dynamicPlugin = definePlugin({ id: "dynamic-plugin", name: "Dynamic Plugin", hooks: (context) => { const startTime = Date.now(); return { onSuccess: () => { console.log(`Request to ${context.initURL} took ${Date.now() - startTime}ms`); }, }; }, }); ``` When multiple plugins are registered, their `setup` functions and hooks execute in the order they appear in the plugins array. Middleware [#middleware] Plugins can define middleware to wrap internal operations. Currently, CallApi supports fetch middleware for intercepting requests at the network layer. See [Middleware](/docs/middlewares) for detailed information. Define middleware using the `middlewares` property. Like hooks, middlewares can be an object or a function that receives the setup context. **Static Middleware:** ```ts title="plugins.ts" import { definePlugin } from "@zayne-labs/callapi/utils"; const loggingPlugin = definePlugin({ id: "logging", name: "Logging Plugin", version: "1.0.0", middlewares: { fetchMiddleware: (ctx) => async (input, init) => { console.log("→", init?.method || "GET", input); const response = await ctx.fetchImpl(input, init); console.log("←", response.status, input); return response; }, }, }); ``` **Dynamic Middleware:** Middlewares can also be a function that receives the plugin setup context: ```ts twoslash title="plugins.ts" import { type PluginSetupContext } from "@zayne-labs/callapi"; import { definePlugin } from "@zayne-labs/callapi/utils"; const cachingPlugin = definePlugin({ id: "caching", name: "Caching Plugin", version: "1.0.0", middlewares: (context: PluginSetupContext) => { const cache = new Map(); return { fetchMiddleware: (ctx) => async (input, init) => { const key = input.toString(); if (cache.has(key)) { return cache.get(key)!.clone(); } const response = await ctx.fetchImpl(input, init); cache.set(key, response.clone()); return response; }, }; }, }); ``` Defining Extra Options [#defining-extra-options] Plugins can define custom options that users can pass to `callApi`. Use `defineExtraOptions` to return a validation schema (like Zod) that defines these options. Here's a plugin that adds an `apiVersion` option to automatically set the API version header: ```ts twoslash title="plugins.ts" import { createFetchClient, type PluginHooks, type PluginSetupContext } from "@zayne-labs/callapi"; import { definePlugin } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const apiVersionSchema = z.object({ apiVersion: z.literal(["v1", "v2", "v3"]).optional(), }); const apiVersionPlugin = definePlugin({ id: "api-version-plugin", name: "API Version Plugin", description: "Adds API version header to requests", version: "1.0.0", defineExtraOptions: () => apiVersionSchema, hooks: { onRequest: (ctx) => { const version = ctx.options.apiVersion ?? "v1"; console.log("API Version:", version); }, } satisfies PluginHooks<{ InferredExtraOptions: typeof apiVersionSchema; }>, setup: (ctx: PluginSetupContext<{ InferredExtraOptions: typeof apiVersionSchema }>) => { const version = ctx.options.apiVersion ?? "v1"; return { request: { ...ctx.request, headers: { ...ctx.request.headers, "X-API-Version": version }, }, }; }, }); const callBackendApi = createFetchClient({ baseURL: "https://api.example.com", plugins: [apiVersionPlugin], apiVersion: "v2", // Default for all requests }); // Use default v2 const { data: users } = await callBackendApi("/users"); // Override to v3 for this request const { data: posts } = await callBackendApi("/posts", { apiVersion: "v3", }); ``` You can use the Zod schema to validates the `apiVersion` option and hence make the type available within at the base or instance level of `callApi`. To ensure your plugin has proper TypeScript support, you can use generic types like `PluginHook`, `PluginMiddlewares`, or `PluginSetupContext`. Simply pass your extra options schema (or its inferred type) to the `InferredExtraOptions` type parameter, like shown in the example above. This makes your custom options available with full type safety throughout the plugin. Alternatively, you can apply the `CallApiPlugin` type with the `InferredExtraOptions` to your entire plugin object using the `satisfies` operator. This provides the inferred type across the entire plugin without needing to specify it for each individual section. When using this pattern, the `definePlugin` helper becomes optional since TypeScript will enforce type safety through the `satisfies` operator, as explained in the [Creating a Plugin](#creating-a-plugin) section. ```ts twoslash title="plugins.ts" import { createFetchClient, type CallApiPlugin } from "@zayne-labs/callapi"; import { z } from "zod"; const apiVersionSchema = z.object({ apiVersion: z.literal(["v1", "v2", "v3"]).optional(), }); const apiVersionPlugin = { id: "api-version-plugin", name: "API Version Plugin", description: "Adds API version header to requests", version: "1.0.0", defineExtraOptions: () => apiVersionSchema, hooks: { onRequest: (ctx) => { const version = ctx.options.apiVersion ?? "v1"; console.log("API Version:", version); }, }, // Look ma! No need to type the extra options individually for each section! setup: (ctx) => { const version = ctx.options.apiVersion ?? "v1"; return { request: { ...ctx.request, headers: { ...ctx.request.headers, "X-API-Version": version }, }, }; }, } satisfies CallApiPlugin<{ InferredExtraOptions: typeof apiVersionSchema }>; ``` Example: Metrics Plugin [#example-metrics-plugin] Here's a complete example of a plugin that tracks API metrics: ```ts title="plugins.ts" import { definePlugin } from "@zayne-labs/callapi/utils"; declare module "@zayne-labs/callapi" { interface Register { meta: { startTime: number; }; } } const metricsPlugin = definePlugin({ id: "metrics", name: "Metrics Plugin", description: "Tracks API response times and success rates", setup: ({ initURL, options }) => { console.info(`Starting request to ${initURL}`); const startTime = performance.now(); return { options: { ...options, meta: { startTime }, }, }; }, hooks: { onSuccess: ({ options }) => { const startTime = options.meta?.startTime ?? 0; const duration = performance.now() - startTime; console.info(`Request completed in ${duration}ms`); }, onError: ({ error, options }) => { const startTime = options.meta?.startTime ?? 0; const duration = performance.now() - startTime; console.error(`Request failed after ${duration}ms:`, error); }, }, }); ``` Types [#types] TCallApiContext[\"InferredExtraOptions\"]) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "description", "description": "A description for the plugin", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "hooks", "description": "Hooks for the plugin", "tags": [], "type": "PluginHooks | ((context: PluginSetupContext) => Awaitable> | Awaitable) | undefined", "simplifiedType": "function | PluginHooks", "required": false, "deprecated": false }, { "name": "id", "description": "A unique id for the plugin", "tags": [], "type": "string", "simplifiedType": "string", "required": true, "deprecated": false }, { "name": "middlewares", "description": "Middlewares that for the plugin", "tags": [], "type": "PluginMiddlewares | ((context: PluginSetupContext) => Awaitable> | Awaitable) | undefined", "simplifiedType": "function | PluginMiddlewares", "required": false, "deprecated": false }, { "name": "name", "description": "A name for the plugin", "tags": [], "type": "string", "simplifiedType": "string", "required": true, "deprecated": false }, { "name": "schema", "description": "Base schema for the client.", "tags": [], "type": "BaseCallApiSchemaAndConfig | undefined", "simplifiedType": "BaseCallApiSchemaAndConfig", "required": false, "deprecated": false }, { "name": "setup", "description": "A function that will be called when the plugin is initialized. This will be called before the any of the other internal functions.", "tags": [], "type": "((context: PluginSetupContext) => Awaitable> | Awaitable) | undefined", "simplifiedType": "function", "required": false, "deprecated": false }, { "name": "version", "description": "A version for the plugin", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false } ] }} /> # CallApi: Request & Response Helpers URL: /docs/request-and-response-helpers Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/request-and-response-helpers.mdx Content type handling, response conversion, and request body processing CallApi automatically handles content types and response parsing with utilities for common data formats. Automatic Content-Type Detection and Assignment [#automatic-content-type-detection-and-assignment] Request bodies are automatically assigned the correct `Content-Type` header: * **Objects** → `application/json` * **Query strings** → `application/x-www-form-urlencoded` * **FormData** → `multipart/form-data` (browser handled) ```ts title="content-types.ts" import { callApi } from "@zayne-labs/callapi"; // Automatically sets Content-Type: application/json await callApi("/api/users", { method: "POST", body: { name: "John", age: 30 }, }); // Automatically sets Content-Type: application/x-www-form-urlencoded await callApi("/api/form", { method: "POST", body: "name=John&age=30", }); // Override when needed await callApi("/api/custom", { method: "POST", body: data, headers: { "Content-Type": "application/custom+json" }, }); ``` Smart Response Parsing [#smart-response-parsing] Responses are automatically parsed based on their `Content-Type` header: * **JSON types** (`application/json`, `application/vnd.api+json`) → Parsed as JSON * **Text types** (`text/*`, `application/xml`) → Parsed as text * **Everything else** → Parsed as blob ```ts title="response-parsing.ts" import { callApi } from "@zayne-labs/callapi"; // Automatically parsed based on Content-Type response header const { data: user } = await callApi("/api/user"); // JSON const { data: html } = await callApi("/page.html"); // Text const { data: image } = await callApi("/avatar.png"); // Blob ``` Manual Response Type Override [#manual-response-type-override] You can still manually specify the response type if needed: Available response types include: * All [response types](https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_methods) from the Fetch API: * `json()` (default fallback) * `text()` * `blob()` * `arrayBuffer()` * `formData()` * `stream` - Returns the direct [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/Response/body) ```ts title="api.ts" import { callApi } from "@zayne-labs/callapi"; const { data: imageBlob } = await callApi("/image", { responseType: "blob", }); const { data: rawText } = await callApi("/data.json", { responseType: "text", // Get JSON as raw text }); const { data: buffer } = await callApi("/binary", { responseType: "arrayBuffer", }); const { data: stream } = await callApi("/large-file", { responseType: "stream", // ReadableStream for progressive processing }); ``` Custom Response Parser [#custom-response-parser] Use a custom parser function: ```ts title="custom-parser.ts" import { callApi } from "@zayne-labs/callapi"; const { data } = await callApi("/api/data", { responseParser: (responseString) => customParser(responseString), }); ``` Result Modes [#result-modes] The `resultMode` option dictates how CallApi processes and returns the final result: * **"all"** (default): Returns `{ data, error, response }`. Standard lifecycle. * **"onlyData"**: Returns only the data from the response. * **"onlyResponse"**: Returns only the `Response` object. * **"fetchApi"**: Also returns only the `Response` object, but also skips parsing of the response body internally and data/errorData schema validation. * **"withoutResponse"**: Returns `{ data, error }`. Standard lifecycle, but omits the `response` property. The fetchApi Mode [#the-fetchapi-mode] The `fetchApi` mode is designed for scenarios where you want most of the library's benefits (URL resolution, plugins, hooks) don't want any internal parsing of the response body to occur, just like the `Fetch Api`. When set to `fetchApi`: 1. **No Parsing**: The library will not attempt to read or parse the response body. 2. **No Validation**: Both data error-data validation are skipped. By default, simplified modes (`"onlyData"`, `"onlyResponse"`, `"fetchApi"`) do not throw errors. Success/failure should be handled via hooks or by checking the return value (e.g., `if (data)` or `if (response?.ok)`). To force an exception, set `throwOnError: true`. Request Body Utilities [#request-body-utilities] Object Bodies [#object-bodies] Objects are automatically JSON stringified: ```ts title="object-bodies.ts" import { callApi } from "@zayne-labs/callapi"; // CallApi handles this automatically await callApi("/api/user", { method: "POST", body: { name: "John", age: 30 }, }); // Equivalent to manual fetch: // fetch("/api/user", { // method: "POST", // headers: { "Content-Type": "application/json" }, // body: JSON.stringify({ name: "John", age: 30 }), // }); ``` Custom Body Serializer [#custom-body-serializer] Override the default JSON serialization: ```ts title="custom-serializer.ts" import { callApi } from "@zayne-labs/callapi"; await callApi("/api/data", { method: "POST", body: { name: "John", age: 30 }, bodySerializer: (body) => customSerialize(body), }); ``` Query String Bodies [#query-string-bodies] Convert objects to URL-encoded strings: ```ts title="query-string-bodies.ts" import { callApi } from "@zayne-labs/callapi"; import { toQueryString } from "@zayne-labs/callapi/utils"; await callApi("/api/search", { method: "POST", body: toQueryString({ name: "John", age: 30 }), }); // Body: "name=John&age=30" // Content-Type: application/x-www-form-urlencoded ``` FormData Bodies [#formdata-bodies] Convert objects to FormData with intelligent type handling: ```ts title="formdata-bodies.ts" import { callApi } from "@zayne-labs/callapi"; import { toFormData } from "@zayne-labs/callapi/utils"; await callApi("/api/upload", { method: "POST", body: toFormData({ avatar: imageFile, // Files/blobs added directly tags: ["dev", "designer"], // Arrays become multiple entries metadata: { role: "admin" }, // Objects are JSON stringified name: "John", // Primitives added as-is }), }); ``` **How toFormData handles different types:** * **Files/Blobs** → Added directly * **Arrays** → Multiple entries with same key * **Objects** → JSON stringified * **Primitives** → Added as-is # CallApi: Request Deduplication URL: /docs/request-dedupe Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/request-dedupe.mdx Optimize your API calls by preventing duplicate requests Request deduplication prevents redundant API calls when multiple identical requests are made to the same endpoint simultaneously or in quick succession. For example, if a user rapidly clicks a "Refresh Profile" button, without deduplication it would trigger multiple API calls for the same data, wasting bandwidth and potentially causing race conditions. With deduplication enabled, duplicate calls are intelligently handled based on your chosen strategy. **Key benefits:** * Prevents duplicate API calls * Reduces network traffic * Prevents race conditions How it works [#how-it-works] Request deduplication works by: 1. Generating a unique key for each request based on URL and parameters (customizable via [dedupeKey](#custom-deduplication-key)) 2. Tracking in-flight requests using this key 3. Handling duplicate requests according to your chosen strategy Requests are only deduplicated when made from the same `callApi` instance. Requests made from different instances will be handled independently. ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callApiOne = createFetchClient(); const callApiTwo = createFetchClient(); const callApiThree = createFetchClient(); // @log: Shared deduplication - these requests are from the same callApi instance, so they will be deduped according to the strategy const resultOne = callApiOne("/users"); const resultTwo = callApiOne("/users"); // Will dedupe with resultOne // @log: Independent deduplication - these requests are from different callApi instances, therefore no deduplication occurs between them const resultThree = callApiTwo("/users"); // Independent deduplication const resultFour = callApiThree("/users"); // Independent deduplication ``` Usage [#usage] CallApi provides three deduplication strategies: 1. Cancel Strategy (Default) [#1-cancel-strategy-default] * Cancels any existing request when a new identical request is made * Best for scenarios where you only need the latest data ```ts title="api.ts" const { data } = await callMainApi("/users/123", { dedupeStrategy: "cancel", }); ``` Cancel Strategy Visualization 2. Defer Strategy [#2-defer-strategy] Shares the same response promise between duplicate requests. If a request is made while an identical one is in-flight, it receives the same response. Defer Strategy Visualization Example: Multiple components requesting user data simultaneously ```tsx title="api.ts" import { useEffect, useState } from "react"; export default function ProfileHeader() { const [userData, setUserData] = useState(null); useEffect(() => { callMainApi("/users/123", { dedupeStrategy: "defer", onSuccess: ({ data }) => setUserData(data), }); }, []); if (!userData) return null; return

{userData.name}

; } function ProfileDetails() { const [userData, setUserData] = useState(null); useEffect(() => { // This will reuse the in-flight request from ProfileHeader callMainApi("/users/123", { dedupeStrategy: "defer", onSuccess: ({ data }) => setUserData(data), }); }, []); if (!userData) return null; return
{userData.email}
; } ``` 3. None Strategy [#3-none-strategy] Disables deduplication, allowing each request to execute independently: ```ts title="api.ts" const { data } = await callMainApi("/users/123", { dedupeStrategy: "none", }); ``` Custom deduplication key [#custom-deduplication-key] By default, CallApi generates a dedupeKey based on the request URL and parameters. Customize this by providing either a static string or a callback function: Static deduplication key [#static-deduplication-key] ```ts title="api.ts" const { data } = await callMainApi("/users/123", { dedupeKey: "custom-key", }); ``` Dynamic deduplication key with callback [#dynamic-deduplication-key-with-callback] For advanced use cases, provide a callback function that receives the request context and returns a custom key: ```ts title="api.ts" // URL and method only - ignore headers and body await callMainApi("/api/user/123", { dedupeKey: (context) => `${context.options.method}:${context.options.fullURL}`, }); // Include specific headers in deduplication await callMainApi("/api/data", { dedupeKey: (context) => { const authHeader = context.request.headers.get("Authorization"); return `${context.options.fullURL}-${authHeader}`; }, }); // User-specific deduplication await callMainApi("/api/dashboard", { dedupeKey: (context) => { const userId = context.options.fullURL.match(/user\/(\d+)/)?.[1]; return `dashboard-${userId}`; }, }); ``` The callback receives a `RequestContext` object with the following properties: * `context.options` - Merged options including URL, method, and other configuration * `context.request` - The request object with headers, body, etc. * `context.baseConfig` - Base configuration from `createFetchClient` * `context.config` - Instance-specific configuration Cache Scope [#cache-scope] By default, deduplication state is isolated per client instance. You can control this using the `dedupeCacheScope` option. Local scope (default) [#local-scope-default] ```ts title="api.ts" // These clients don't share deduplication state const userClient = createFetchClient({ baseURL: "/api/users" }); const postClient = createFetchClient({ baseURL: "/api/posts" }); ``` Global scope [#global-scope] By setting `dedupeCacheScope: "global"`, different client instances can share the same deduplication state. ```ts title="api.ts" // These clients share deduplication state const userClient = createFetchClient({ baseURL: "/api/users", dedupeCacheScope: "global", }); const profileClient = createFetchClient({ baseURL: "/api/profiles", dedupeCacheScope: "global", }); ``` dedupeCacheScopeKey [#dedupecachescopekey] When using global scope, you can group related clients together using `dedupeCacheScopeKey`. Only clients with the same key will share deduplication state. ```ts title="api.ts" // Group related services const userClient = createFetchClient({ baseURL: "/api/users", dedupeCacheScope: "global", dedupeCacheScopeKey: "user-service", }); const profileClient = createFetchClient({ baseURL: "/api/profiles", dedupeCacheScope: "global", // Shares cache with userClient dedupeCacheScopeKey: "user-service", }); // Separate analytics const analyticsClient = createFetchClient({ baseURL: "/api/analytics", dedupeCacheScope: "global", // Different cache namespace dedupeCacheScopeKey: "analytics", }); ``` Recommendations [#recommendations] * Use `cancel` when you only need the most recent request (most common) * Use `defer` when multiple parts of your app need the same data simultaneously * Use `none` when requests must be independent (polling, etc.) Types [#types] string | undefined) | undefined", "simplifiedType": "function | \"default\" | AnyString", "required": false, "deprecated": false }, { "name": "dedupeKey", "description": "Custom key generator for request deduplication.\n\nOverride the default key generation strategy to control exactly which requests\nare considered duplicates. The default key combines URL, method, body, and\nrelevant headers (excluding volatile ones like 'Date', 'Authorization', etc.).\n\n**Default Key Generation:**\nThe auto-generated key includes:\n- Full request URL (including query parameters)\n- HTTP method (GET, POST, etc.)\n- Request body (for POST/PUT/PATCH requests)\n- Stable headers (excludes Date, Authorization, User-Agent, etc.)\n\n**Custom Key Best Practices:**\n- Include only the parts of the request that should affect deduplication\n- Avoid including volatile data (timestamps, random IDs, etc.)\n- Consider performance - simpler keys are faster to compute and compare\n- Ensure keys are deterministic for the same logical request\n- Use consistent key formats across your application\n\n**Performance Considerations:**\n- Function-based keys are computed on every request - keep them lightweight\n- String keys are fastest but least flexible\n- Consider caching expensive key computations if needed", "tags": [ { "name": "example", "text": "```ts\nimport { callApi } from \"@zayne-labs/callapi\";\n\n// Simple static key - useful for singleton requests\nconst config = callApi(\"/api/config\", {\n dedupeKey: \"app-config\",\n dedupeStrategy: \"defer\" // Share the same config across all requests\n});\n\n// URL and method only - ignore headers and body\nconst userData = callApi(\"/api/user/123\", {\n dedupeKey: (context) => `${context.options.method}:${context.options.fullURL}`\n});\n\n// Include specific headers in deduplication\nconst apiCall = callApi(\"/api/data\", {\n dedupeKey: (context) => {\n const authHeader = context.request.headers.get(\"Authorization\");\n return `${context.options.fullURL}-${authHeader}`;\n }\n});\n\n// User-specific deduplication\nconst userSpecificCall = callApi(\"/api/dashboard\", {\n dedupeKey: (context) => {\n const userId = context.options.fullURL.match(/user\\/(\\d+)/)?.[1];\n return `dashboard-${userId}`;\n }\n});\n\n// Ignore certain query parameters\nconst searchCall = callApi(\"/api/search?q=test×tamp=123456\", {\n dedupeKey: (context) => {\n const url = new URL(context.options.fullURL);\n url.searchParams.delete(\"timestamp\"); // Remove volatile param\n return `search:${url.toString()}`;\n }\n});\n```" }, { "name": "default", "text": "Auto-generated from request details" } ], "type": "string | ((context: RequestContext) => string | undefined) | undefined", "simplifiedType": "function | string", "required": false, "deprecated": false }, { "name": "dedupeStrategy", "description": "Strategy for handling duplicate requests. Can be a static string or callback function.\n\n**Available Strategies:**\n- `\"cancel\"`: Cancel previous request when new one starts (good for search)\n- `\"defer\"`: Share response between duplicate requests (good for config loading)\n- `\"none\"`: No deduplication, all requests execute independently", "tags": [ { "name": "example", "text": "```ts\n// Static strategies\nconst searchClient = createFetchClient({\n dedupeStrategy: \"cancel\" // Cancel previous searches\n});\n\nconst configClient = createFetchClient({\n dedupeStrategy: \"defer\" // Share config across components\n});\n\n// Dynamic strategy based on request\nconst smartClient = createFetchClient({\n dedupeStrategy: (context) => {\n return context.options.method === \"GET\" ? \"defer\" : \"cancel\";\n }\n});\n\n// Search-as-you-type with cancel strategy\nconst handleSearch = async (query: string) => {\n try {\n const { data } = await callApi(\"/api/search\", {\n method: \"POST\",\n body: { query },\n dedupeStrategy: \"cancel\",\n dedupeKey: \"search\" // Cancel previous searches, only latest one goes through\n });\n\n updateSearchResults(data);\n } catch (error) {\n if (error.name === \"AbortError\") {\n // Previous search cancelled - (expected behavior)\n return;\n }\n console.error(\"Search failed:\", error);\n }\n};\n\n```" }, { "name": "default", "text": "\"cancel\"" } ], "type": "\"cancel\" | \"defer\" | \"none\" | ((context: RequestContext) => DedupeStrategyUnion) | undefined", "simplifiedType": "function | \"none\" | \"defer\" | \"cancel\"", "required": false, "deprecated": false } ] }} /> # CallApi: Request Options URL: /docs/request-options Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/request-options.mdx All options shared with the Fetch API This page documents the configuration options that callApi shares with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). | Record<\"Content-Type\", CommonContentTypes | undefined> | Record | Record | [string, string][] | undefined", "simplifiedType": "array | Record | Record | Record<\"Content-Type\", \"video/x-msvideo\" | \"video/webm\" | \"video/ogg\" | \"video/mpeg\" | \"video/mp4\" | \"video/mp2t\" | \"video/av1\" | \"video/3gpp2\" | \"video/3gpp\" | \"text/plain\" | \"text/javascript\" | \"text/html\" | \"text/csv\" | \"text/css\" | \"text/calendar\" | \"model/gltf+json\" | \"model/gltf-binary\" | \"image/x-icon\" | \"image/webp\" | \"image/tiff\" | \"image/svg+xml\" | \"image/png\" | \"image/jpeg\" | \"image/gif\" | \"image/bmp\" | \"image/avif\" | \"font/woff2\" | \"font/woff\" | \"font/ttf\" | \"font/otf\" | \"audio/x-midi\" | \"audio/webm\" | \"audio/opus\" | \"audio/ogg\" | \"audio/mpeg\" | \"audio/aac\" | \"application/zip\" | \"application/xml\" | \"application/xhtml+xml\" | \"application/wasm\" | \"application/vnd.ms-fontobject\" | \"application/rtf\" | \"application/pdf\" | \"application/ogg\" | \"application/octet-stream\" | \"application/ld+json\" | \"application/json\" | \"application/gzip\" | \"application/epub+zip\" | AnyString | undefined> | Record<\"Authorization\", `Token ${string}` | `Bearer ${string}` | `Basic ${string}` | undefined> | object", "required": false, "deprecated": false }, { "name": "method", "description": "HTTP method for the request.", "tags": [ { "name": "default", "text": "\"GET\"" } ], "type": "AnyString | \"CONNECT\" | \"DELETE\" | \"GET\" | \"HEAD\" | \"OPTIONS\" | \"PATCH\" | \"POST\" | \"PUT\" | \"TRACE\" | undefined", "simplifiedType": "\"TRACE\" | \"PUT\" | \"POST\" | \"PATCH\" | \"OPTIONS\" | \"HEAD\" | \"GET\" | \"DELETE\" | \"CONNECT\" | AnyString", "required": false, "deprecated": false }, { "name": "cache", "description": "", "tags": [], "type": "RequestCache | undefined", "simplifiedType": "\"reload\" | \"only-if-cached\" | \"no-store\" | \"no-cache\" | \"force-cache\" | \"default\"", "required": false, "deprecated": false }, { "name": "credentials", "description": "", "tags": [], "type": "RequestCredentials | undefined", "simplifiedType": "\"same-origin\" | \"omit\" | \"include\"", "required": false, "deprecated": false }, { "name": "integrity", "description": "", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "keepalive", "description": "", "tags": [], "type": "boolean | undefined", "simplifiedType": "boolean", "required": false, "deprecated": false }, { "name": "mode", "description": "", "tags": [], "type": "RequestMode | undefined", "simplifiedType": "\"no-cors\" | \"navigate\" | \"cors\" | \"same-origin\"", "required": false, "deprecated": false }, { "name": "priority", "description": "", "tags": [], "type": "RequestPriority | undefined", "simplifiedType": "\"low\" | \"high\" | \"auto\"", "required": false, "deprecated": false }, { "name": "redirect", "description": "", "tags": [], "type": "RequestRedirect | undefined", "simplifiedType": "\"manual\" | \"follow\" | \"error\"", "required": false, "deprecated": false }, { "name": "referrer", "description": "", "tags": [], "type": "string | undefined", "simplifiedType": "string", "required": false, "deprecated": false }, { "name": "referrerPolicy", "description": "", "tags": [], "type": "ReferrerPolicy | undefined", "simplifiedType": "\"unsafe-url\" | \"strict-origin-when-cross-origin\" | \"strict-origin\" | \"origin-when-cross-origin\" | \"origin\" | \"no-referrer-when-downgrade\" | \"no-referrer\" | \"same-origin\" | \"\"", "required": false, "deprecated": false }, { "name": "signal", "description": "", "tags": [], "type": "AbortSignal | null | undefined", "simplifiedType": "object | null", "required": false, "deprecated": false }, { "name": "window", "description": "", "tags": [], "type": "null | undefined", "simplifiedType": "null", "required": false, "deprecated": false }, { "name": "duplex", "description": "", "tags": [], "type": "\"half\" | undefined", "simplifiedType": "\"half\"", "required": false, "deprecated": false } ] }} /> # CallApi: Timeout and Retries URL: /docs/timeout-and-retries Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/timeout-and-retries.mdx Configure automatic retries and request timeouts Timeout [#timeout] Set a maximum time limit for requests using the `timeout` option (in milliseconds). If a request takes longer than the specified timeout, it will be aborted and a `TimeoutError` will be returned: ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callBackendApi = createFetchClient({ baseURL: "http://localhost:3000", timeout: 5000, }); const result = await callBackendApi("/api/users", { timeout: 10000, }); ``` Auto Retry [#auto-retry] CallApi can automatically retry failed requests. For most use cases, you only need to specify the number of retry attempts: ```ts title="api.ts" const result = await callApi("/api/users", { retryAttempts: 3, }); ``` Advanced Retry Options [#advanced-retry-options] CallApi provides flexible retry mechanisms with both linear and exponential backoff strategies. Linear Retry Strategy [#linear-retry-strategy] Waits a fixed amount of time between retries: ```ts title="api.ts" const result = await callApi("/api/users", { retryStrategy: "linear", retryAttempts: 3, retryDelay: 1000, }); ``` Exponential Retry Strategy [#exponential-retry-strategy] Increases the delay between retries exponentially. This is the recommended strategy for most APIs to prevent overwhelming servers. ```ts title="api.ts" const result = await callApi("/api/users", { retryStrategy: "exponential", retryAttempts: 5, // Retry up to 5 times retryDelay: 1000, // Start with 1 second delay retryMaxDelay: 10000, // Cap the delay at 10 seconds // Retry delays will be: 1s, 2s, 4s, 8s, 10s (capped at maxDelay) }); ``` Dynamic Retry Delay [#dynamic-retry-delay] Pass a function to `retryDelay` to dynamically calculate the delay based on the current attempt count: ```ts title="api.ts" const result = await callApi("/api/data", { retryAttempts: 5, retryDelay: (attemptCount) => { // Example: Add random jitter to prevent thundering herd const baseDelay = 1000 * 2 ** attemptCount; const jitter = Math.random() * 1000; return Math.min(baseDelay + jitter, 10000); }, }); ``` Retry Methods and Status Codes [#retry-methods-and-status-codes] Customize when to retry a request with `retryMethods` and `retryStatusCodes`: 1. **Retry Methods**: Specifies which HTTP methods should be retried. Defaults to `["GET", "POST"]`. Be careful when configuring this to retry `PUT` or `DELETE` requests unless they are idempotent. 2. **Retry Status Codes**: Specifies which HTTP status codes should be retried. If not specified, all error status codes are eligible for retry. ```ts title="api.ts" const result = await callApi("/api/users", { retryAttempts: 3, retryDelay: 1000, retryMethods: ["GET", "POST"], // Only retry on rate limits and server errors retryStatusCodes: [429, 500, 502, 503, 504], }); ``` Custom Retry Condition [#custom-retry-condition] Use `retryCondition` to implement custom retry logic. This function receives the error context and returns a boolean (or promise) indicating whether to retry: ```ts title="api.ts" const result = await callApi("/api/users", { retryAttempts: 3, retryCondition: ({ error, response }) => { return response?.status === 429; }, }); ``` The onRetry hook [#the-onretry-hook] Listen to retry attempts using the `onRetry` hook: ```ts title="api.ts" const result = await callApi("/todos/1", { retryAttempts: 3, onRetry: ({ retryAttemptCount, error }) => { console.log(`Retrying request (attempt ${retryAttemptCount}). Reason: ${error.message}`); }, }); ``` Types [#types] Timeout [#timeout-1] Retry [#retry] | undefined", "simplifiedType": "RetryCondition", "required": false, "deprecated": false }, { "name": "retryDelay", "description": "Delay between retries in milliseconds", "tags": [ { "name": "default", "text": "1000" } ], "type": "number | ((currentAttemptCount: number) => number) | undefined", "simplifiedType": "function | number", "required": false, "deprecated": false }, { "name": "retryMaxDelay", "description": "Maximum delay in milliseconds. Only applies to exponential strategy", "tags": [ { "name": "default", "text": "10000" } ], "type": "number | undefined", "simplifiedType": "number", "required": false, "deprecated": false }, { "name": "retryMethods", "description": "HTTP methods that are allowed to retry", "tags": [ { "name": "default", "text": "[\"GET\", \"POST\"]" } ], "type": "(AnyString | \"CONNECT\" | \"DELETE\" | \"GET\" | \"HEAD\" | \"OPTIONS\" | \"PATCH\" | \"POST\" | \"PUT\" | \"TRACE\")[] | undefined", "simplifiedType": "array", "required": false, "deprecated": false }, { "name": "retryStatusCodes", "description": "HTTP status codes that trigger a retry", "tags": [], "type": "(AnyNumber | 408 | 409 | 425 | 429 | 500 | 502 | 503 | 504)[] | undefined", "simplifiedType": "array", "required": false, "deprecated": false }, { "name": "retryStrategy", "description": "Strategy to use when retrying", "tags": [ { "name": "default", "text": "\"linear\"" } ], "type": "\"exponential\" | \"linear\" | undefined", "simplifiedType": "\"linear\" | \"exponential\"", "required": false, "deprecated": false } ] }} /> # CallApi: URL helpers URL: /docs/url-helpers Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/url-helpers.mdx Learn about various convenient ways to build request URLs in CallApi Base URL [#base-url] Set a base URL for requests using the `baseURL` option: ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; const callBackendApi = createFetchClient({ baseURL: "https://api.example.com", }); const { data } = await callBackendApi("/users/123/posts"); // @annotate: The resolved URL will be: "https://api.example.com/users/123/posts" ``` Dynamic Parameters [#dynamic-parameters] Many URLs contain dynamic parts representing specific resources, like an ID. For example, `/users/123` fetches user 123, and `/posts/456` fetches post 456. Instead of manually building strings like `"/users/" + userId` or using template literals, CallApi allows you to use dynamic parameter placeholders with a `:` prefix (e.g., `/users/:userId`). This makes your code cleaner and more maintainable. Provide actual values for these placeholders using the `params` option. CallApi automatically replaces the placeholders with the provided values. The `params` option accepts either an object or an array: * **Object**: The keys should match the parameter names (without the `:`). For example, if your URL is `/users/:userId/posts/:postId`, passing `params: { userId: 123, postId: 456 }` will result in `/users/123/posts/456`. * **Array**: The values replace the parameters in the order they appear in the URL. For example, if your URL is `/users/:userId/posts/:postId`, passing `params: ['123', '456']` will result in `/users/123/posts/456`. Using an object is generally recommended as it's clearer which value goes with which parameter. ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; const { data } = await callApi("https://api.example.com/users/:userId/posts/:postId", { params: { userId: 123, postId: 456, }, }); const { data: userData } = await callApi("https://api.example.com/users/:userId/posts/:postId", { params: [123, 456], }); // @annotate: The resolved URL for both cases will be: "https://api.example.com/users/123/posts/456" ``` Query Parameters [#query-parameters] Include query parameters in the URL using the `query` option: ```ts twoslash title="api.ts" import { callApi } from "@zayne-labs/callapi"; const { data } = await callApi("https://api.example.com/users/123/posts", { query: { page: 1, limit: 10, sort: "latest", }, }); // @annotate: The resolved URL will be: "https://api.example.com/users/123/posts?page=1&limit=10&sort=latest" ``` Method Prefixes [#method-prefixes] CallApi provides a convenient way to specify HTTP methods directly in the URL using the `@method/` prefix. This allows you to: * Write more concise API calls by embedding the HTTP method in the URL * Make your code more readable by keeping the HTTP method close to the endpoint Usage [#usage] ```ts title="api.ts" import { callApi } from "@zayne-labs/callapi"; // Using method prefix const result = await callApi("@delete/users/123"); // Equivalent to: const result2 = await callApi("users/123", { method: "DELETE", }); ``` How It Works [#how-it-works] When you prefix a URL with `@method/` (e.g., `@get/users`): 1. The method (e.g., `get`, `post`, `put`, etc.) is extracted from the URL 2. The extracted method is automatically set as the request method 3. The remaining part of the URL is used as the endpoint Supported Methods [#supported-methods] CallApi supports the following HTTP methods via URL prefixes: * `@get/` → GET requests * `@post/` → POST requests * `@put/` → PUT requests * `@delete/` → DELETE requests * `@patch/` → PATCH requests Any other method prefix (like `@head/`, `@options/`, `@trace/`) will be ignored and the URL will fall back to the default GET method. {/* prettier-ignore */} * Always include a forward slash after the method prefix (e.g., `@get/` not `@get`) * If both a method prefix and explicit `method` option are provided, the explicit method will be used Types [#types] | (string | number | boolean)[] | undefined", "simplifiedType": "array | Record", "required": false, "deprecated": false }, { "name": "query", "description": "Query parameters to append to the URL as search parameters.\n\nThese will be serialized into the URL query string using standard\nURL encoding practices.", "tags": [ { "name": "example", "text": "```typescript\n// Basic query parameters\nconst queryOptions: URLOptions = {\n initURL: \"/users\",\n query: {\n page: 1,\n limit: 10,\n search: \"john doe\",\n active: true\n }\n};\n// Results in: /users?page=1&limit=10&search=john%20doe&active=true\n\n// Filtering and sorting\nconst filterOptions: URLOptions = {\n initURL: \"/products\",\n query: {\n category: \"electronics\",\n minPrice: 100,\n maxPrice: 500,\n sortBy: \"price\",\n order: \"asc\"\n }\n};\n// Results in: /products?category=electronics&minPrice=100&maxPrice=500&sortBy=price&order=asc\n```" } ], "type": "Record | URLSearchParams | undefined", "simplifiedType": "object | Record", "required": false, "deprecated": false } ] }} /> # CallApi: Validation URL: /docs/validation Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/validation.mdx Define validation schemas for your requests details and response data Validation schemas let you pre-define URL paths and validate the shape of request and response data. This validation happens at both the type-level (TypeScript) and at runtime, helping you catch errors early, ensure data integrity, and document your API structure. CallApi uses Standard Schema internally, allowing you to bring your own Standard Schema-compliant validator like Zod, Valibot, or ArkType. Basic Usage [#basic-usage] To create a validation schema, you need to import the `defineSchema` function from `@zayne-labs/callapi`. ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; export const baseSchema = defineSchema({ // [!code highlight] "/path": { // [!code highlight] body: z.object({ // [!code highlight] userId: z.string(), // [!code highlight] id: z.number(), // [!code highlight] title: z.string(), // [!code highlight] completed: z.boolean(), // [!code highlight] }), // [!code highlight] // [!code highlight] data: z.object({ // [!code highlight] userId: z.string(), // [!code highlight] id: z.number(), // [!code highlight] title: z.string(), // [!code highlight] completed: z.boolean(), // [!code highlight] }), // [!code highlight] }, // [!code highlight] }); const callApi = createFetchClient({ baseURL: "https://jsonplaceholder.typicode.com", schema: baseSchema, // [!code highlight] }); ``` Validation Schema [#validation-schema] The validation schema is a map of paths to their validation rules. Each path can define validation for different parts of the request and response. **Response Validation:** * `data` - Validates successful response data * `errorData` - Validates error response data When validation fails, CallApi throws a `ValidationError` with details about what went wrong. ```ts twoslash title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/products/:id": { data: z.object({ id: z.number(), title: z.string(), price: z.number(), }), errorData: z.object({ code: z.string(), message: z.string(), }), }, }), }); const { data, error } = await callApi("/products/:id", { params: { id: 100, }, }); // @annotate: data will be typed as { id: number; title: string; price: number } // @annotate: errorData for HTTP errors will be typed as { code: string; message: string } ``` **Request Validation:** * `body` - Validates request body data before sending * `headers` - Ensures required headers are present and correctly formatted * `method` - Validates or enforces specific HTTP methods (GET, POST, etc.) * `params` - Validates URL parameters (`:param`) * `query` - Validates query string parameters before adding to the URL ```ts title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/users/:userId": { query: z.object({ id: z.string(), }), params: z.object({ userId: z.string(), }), }, }), }); ``` Body Validation [#body-validation] The `body` key validates request body data and provides type safety for the request body: ```ts twoslash title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/products": { body: z.object({ title: z.string(), price: z.number(), category: z.string(), }), }, }), }); // @errors: 2739 const { data } = await callApi("/products", { body: {}, }); ``` Headers Validation [#headers-validation] The `headers` key validates request headers, useful for enforcing required headers or validating header formats: ```ts twoslash title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/products": { headers: z.object({ "x-api-key": z.string(), "content-type": z.literal("application/json"), authorization: z.string().startsWith("Bearer "), }), }, }), }); // @errors: 2322 const { data } = await callApi("/products", { headers: {}, }); ``` Meta Validation [#meta-validation] The `meta` key validates the meta option, which passes arbitrary metadata through the request lifecycle: ```ts twoslash title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/analytics": { meta: z.object({ trackingId: z.string(), userId: z.string().optional(), }), }, }), }); // @errors: 2741 const { data } = await callApi("/analytics", { meta: {}, }); ``` Query Parameters [#query-parameters] The `query` schema validates query parameters before they're added to the URL: ```ts twoslash title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/products": { query: z.object({ category: z.string(), page: z.number(), limit: z.number(), }), }, }), }); // @errors: 2739 const { data } = await callApi("/products", { query: {}, }); ``` Dynamic Path Parameters [#dynamic-path-parameters] The `params` schema validates URL parameters. You can define dynamic parameters in two ways: 1. Using colon syntax in the schema path (`:paramName`) - Enforces types at the type level only 2. Using the `params` validator schema - Validates at both type level and runtime (takes precedence over colon syntax) ```ts twoslash title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ // Using colon syntax "/products/:id": { data: z.object({ name: z.string(), price: z.string(), }), }, // Using params schema "/products": { data: z.object({ name: z.string(), price: z.number(), }), params: z.object({ version: z.string(), }), }, // Using both colon syntax and params schema (the params schema takes precedence and ensures validation both at type level and runtime) "/products/:id/:category": { data: z.object({ name: z.string(), price: z.number(), }), params: z.object({ id: z.number(), category: z.string(), }), }, }), }); const response1 = await callApi("/products/:id", { params: { id: 20, }, }); const response2 = await callApi("/products", { params: { version: "v1", }, }); const response3 = await callApi("/products/:id/:category", { params: { id: 20, category: "electronics", }, }); ``` HTTP Method Modifiers [#http-method-modifiers] You can specify the HTTP method in two ways: 1. Using the `method` validator schema 2. Prefixing the path with `@method-name` (supported: `@get/`, `@post/`, `@put/`, `@patch/`, `@delete/`) When using the `@method-name/` prefix, it's automatically added to request options. You can override it by explicitly passing the `method` option to `callApi`. ```ts title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ // Using method prefix "@post/products": { body: z.object({ title: z.string(), price: z.number(), }), }, // Using method validator "products/:id": { method: z.literal("DELETE"), }, }), }); ``` Validation Schema Per Instance [#validation-schema-per-instance] You can define a validation schema for a specific request instead of globally on `createFetchClient`: ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", }); const { data, error } = await callApi("/user", { schema: { data: z.object({ userId: z.string(), id: z.number(), title: z.string(), completed: z.boolean(), }), }, }); ``` Custom Validators [#custom-validators] Instead of using Zod schemas, you can also provide custom validator functions for any schema field. These functions receive the input value and can perform custom validation or transformation. They can also be `async` if need be. ```ts twoslash title="client.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; // Simulation: Get allowed domains from config/API const waitUntil = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)); const getAllowedDomains = async () => { await waitUntil(1000); return ["example.com", "company.com"]; }; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/users": { // Async body validator with custom validation body: async (body) => { if (!body || typeof body !== "object") { throw new Error("Invalid request body"); } // Required fields if (!("name" in body) || typeof body.name !== "string") { throw new Error("Name is required"); } if (!("email" in body) || typeof body.email !== "string" || !body.email.includes("@")) { throw new Error("Valid email required"); } // Validate domain against allowed list const domain = body.email.split("@")[1] ?? ""; const allowed = await getAllowedDomains(); if (!allowed.includes(domain)) { throw new Error(`Email domain ${domain} not allowed`); } return { email: body.email.toLowerCase(), name: body.name.trim(), }; }, // Response data validator data: (data) => { if ( !data || typeof data !== "object" || !("id" in data) || !("name" in data) || !("email" in data) ) { throw new Error("Invalid response data"); } return data; // Type will be narrowed to { id: number; name: string; email: string } }, }, }), }); // @annotate: Types are inferred from validator return types const { data } = await callApi("/users", { body: { email: "JOHN@example.com", name: " John ", // Will be trimmed & lowercased. }, }); ``` Custom validators allow you to: 1. Accept raw input data to validate 2. Run sync or async validation logic 3. Transform data if needed (e.g., normalize, sanitize) 4. Can throw errors for invalid data 5. Return the validated data (From which TypeScript infers the return type) Overriding the base schema for a specific path [#overriding-the-base-schema-for-a-specific-path] You can override the base schema by passing a schema to the `schema` option: ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/user": { data: z.object({ name: z.string(), id: z.number(), }), }, }), }); const { data, error } = await callApi("/user", { // @annotate: This will override the base schema for this specific path schema: { data: z.object({ id: z.number(), address: z.string(), isVerified: z.boolean(), }), }, }); ``` Extending the base schema for a specific path [#extending-the-base-schema-for-a-specific-path] In case you want to extend the base schema instead of overriding it, you can pass a callback to schema option of the instance, which will be called with the base schema and the specific schema for current path: ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ baseURL: "https://api.example.com", schema: defineSchema({ "/user": { data: z.object({ userId: z.string(), id: z.number(), title: z.string(), completed: z.boolean(), }), }, }), }); const { data, error } = await callApi("/user", { // @annotate: This will extend the base schema for this specific path schema: ({ currentRouteSchema }) => ({ ...currentRouteSchema, errorData: z.object({ code: z.string(), message: z.string(), }), }), }); ``` Fallback Route Schema [#fallback-route-schema] You can define a fallback schema that applies to all routes not explicitly defined in your schema using the special `@default` key (or use the exported `fallBackRouteSchemaKey` constant): ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ schema: defineSchema({ // Fallback schema for all routes "@default": { headers: z.object({ "x-api-key": z.string(), }), }, // Specific route schema (takes precedence over fallback) "/users": { data: z.object({ id: z.number(), name: z.string(), }), }, }), }); // This will use the fallback schema (requires x-api-key header) await callApi("/posts"); // This will use both the fallback and specific schema await callApi("/users"); ``` Alternatively, use the exported constant for better maintainability: ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { fallBackRouteSchemaKey } from "@zayne-labs/callapi/constants"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ schema: defineSchema({ [fallBackRouteSchemaKey]: { headers: z.object({ "x-api-key": z.string(), }), }, "/users": { data: z.object({ id: z.number(), name: z.string(), }), }, }), }); ``` Strict Mode [#strict-mode] By default, CallAPI allows requests to paths not defined in the schema. Enable strict mode to prevent usage of undefined paths: ```ts twoslash title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const callApi = createFetchClient({ schema: defineSchema( { "/user": { data: z.object({ userId: z.string(), id: z.number(), title: z.string(), completed: z.boolean(), }), }, }, { // [!code highlight] strict: true, // [!code highlight] } ), }); // @errors: 2345 const { data, error } = await callApi("/invalid-path"); ``` Validation Error Handling [#validation-error-handling] CallAPI throws a `ValidationError` when validation fails. The error includes detailed information about what failed: ```ts title="client.ts" import { isValidationError } from "@zayne-labs/callapi/utils"; const { data, error } = await callApi("/products/:id", { params: { id: "invalid" }, // Should be a number }); if (isValidationError(error)) { console.log(error.errorData); // Validation issues array console.log(error.issueCause); // Which schema key caused the validation error } ``` Types [#types] CallApiSchema [#callapischema] | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "body", "description": "The schema to use for validating the request body.", "tags": [], "type": "CallApiSchemaType | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "data", "description": "The schema to use for validating the response data.", "tags": [], "type": "CallApiSchemaType | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "errorData", "description": "The schema to use for validating the response error data.", "tags": [], "type": "CallApiSchemaType | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "headers", "description": "The schema to use for validating the request headers.", "tags": [], "type": "CallApiSchemaType | Record<\"Content-Type\", CommonContentTypes | undefined> | Record | Record | [string, string][]> | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "meta", "description": "The schema to use for validating the meta option.", "tags": [], "type": "CallApiSchemaType | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "method", "description": "The schema to use for validating the request method.", "tags": [], "type": "CallApiSchemaType | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "params", "description": "The schema to use for validating the request url parameters.", "tags": [], "type": "CallApiSchemaType | (string | number | boolean)[]> | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false }, { "name": "query", "description": "The schema to use for validating the request url queries.", "tags": [], "type": "CallApiSchemaType> | undefined", "simplifiedType": "function | object", "required": false, "deprecated": false } ] }} /> CallApiSchemaConfig [#callapischemaconfig] # CallApi: Integrations URL: /docs/integrations Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/integrations/index.mdx Learn how to use CallApi with various libraries and frameworks CallApi works seamlessly with any library that expects a Promise-based HTTP client, since it's built on the native Fetch API. Choose an integration guide from the sidebar to learn more about specific details and best practices. Common Integration Pattern [#common-integration-pattern] Most integrations follow this pattern: 1. Create a CallApi instance with your base configuration 2. Use the instance in your data fetching hooks or functions 3. Handle errors using CallApi's error handling utilities Example: ```tsx title="example.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { isHTTPErrorInstance } from "@zayne-labs/callapi/utils"; const callMainApi = createFetchClient({ baseURL: "https://api.example.com", resultMode: "onlyData", // Return just data, not { data, error, response } throwOnError: true, // Libraries like React Query expect thrown errors }); export default function App() { // React Query const queryResult = useQuery({ queryKey: ["user", userId], queryFn: () => callMainApi(`/users/${userId}`), }); // SWR const swrResult = useSWR(`/users/${userId}`, () => callMainApi(`/users/${userId}`)); return (

{queryResult.data.name}

{swrResult.data.name}

); } ``` # CallApi: React Query URL: /docs/integrations/react-query Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/integrations/react-query.mdx Learn how to use CallApi with React Query This guide covers essential patterns and best practices for using CallApi with [React Query](https://tanstack.com/query/v5/docs/framework/react), combining CallApi's features with React Query's caching and synchronization capabilities. Quick Start [#quick-start] By default, CallApi returns errors as values in the result object instead of throwing them, which is great for explicit error handling. However, React Query expects query functions to either return data on success or throw an error on failure. To make CallApi work seamlessly with React Query, configure it to throw errors and return only the data: ```ts title="api/todos.ts" import { useQuery } from "@tanstack/react-query"; import { callApi } from "@zayne-labs/callapi"; type Todo = { completed: boolean; id: number; title: string; }; export const useTodos = () => { return useQuery({ queryKey: ["todos"], queryFn: () => { return callApi("/todos", { throwOnError: true, resultMode: "onlyData", }); }, }); }; ``` Configuration Options [#configuration-options] Key options for React Query integration: * **`throwOnError: true`** - Makes CallApi throw errors instead of returning them * **`resultMode: "onlyData"`** - Returns just the data property, perfect for React Query These settings ensure CallApi behaves exactly like React Query expects: throwing errors for failures and returning clean data for successes. Centralized Configuration [#centralized-configuration] ```ts title="api/client.ts" import { createFetchClient } from "@zayne-labs/callapi"; export const callApiForQuery = createFetchClient({ baseURL: "https://api.example.com", // Default to React Query compatible settings throwOnError: true, resultMode: "onlyData", }); // Use the configured client in queries export const useTodos = () => { return useQuery({ queryKey: ["todos"], queryFn: () => callApiForQuery("/todos"), }); }; ``` Data Inference via typescript [#data-inference-via-typescript] Option 1: Schema Validation (Recommended) [#option-1-schema-validation-recommended] Using validation libraries like Zod provides both runtime safety and automatic type inference: ```ts title="hooks/useTodos.ts" import { useQuery } from "@tanstack/react-query"; import { callApi } from "@zayne-labs/callapi"; import { z } from "zod"; const todosSchema = z.array( z.object({ id: z.number(), title: z.string(), completed: z.boolean(), }) ); export const useTodos = () => { return useQuery({ queryKey: ["todos"], queryFn: () => { return callApi("/todos", { schema: { data: todosSchema }, throwOnError: true, resultMode: "onlyData", }); }, }); }; ``` Option 2: Manual Type Specification [#option-2-manual-type-specification] ```ts title="hooks/useTodos.ts" import { useQuery } from "@tanstack/react-query"; import { callApi } from "@zayne-labs/callapi"; type Todo = { completed: boolean; id: number; title: string; }; export const useTodos = () => { return useQuery({ queryKey: ["todos"], queryFn: () => { // Pass `false` as second generic to signal errors will be thrown allow callApi to return the expected type // This is needed due to TypeScript limitations with partial generic inference // See: https://github.com/microsoft/TypeScript/issues/26242 return callApi("@get/todos", { throwOnError: true, resultMode: "onlyData", }); }, }); }; ``` # CallApi: Runtime Helpers URL: /docs/utilities/runtime-helpers Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/utilities/runtime-helpers.mdx Helper functions and utilities for working with CallApi CallApi provides utility functions for schemas, plugins, error handling, and data processing. Schema & Plugin Utilities [#schema--plugin-utilities] defineSchema [#defineschema] Creates type-safe schema configurations: ```ts title="api-schema.ts" import { fallBackRouteSchemaKey } from "@zayne-labs/callapi/constants"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; export const apiSchema = defineSchema({ [fallBackRouteSchemaKey]: { headers: z.object({ "x-api-key": z.string(), }), }, "/users/:id": { data: z.object({ id: z.number(), name: z.string(), email: z.string().email(), }), params: z.object({ id: z.number(), }), }, }); ``` definePlugin [#defineplugin] Creates type-safe plugins: ```ts title="auth-plugin.ts" import { definePlugin } from "@zayne-labs/callapi/utils"; import { z } from "zod"; export const authPlugin = definePlugin({ id: "auth-plugin", name: "Authentication Plugin", defineExtraOptions: () => z.object({ apiKey: z.string(), refreshToken: z.string().optional(), }), setup: ({ options, request }) => ({ request: { ...request, headers: { ...request.headers, Authorization: `Bearer ${options.apiKey}`, }, }, }), }); ``` Data Processing Utilities [#data-processing-utilities] toFormData [#toformdata] Converts objects to FormData with intelligent type handling: ```ts title="form-data.ts" import { toFormData } from "@zayne-labs/callapi/utils"; const formData = toFormData({ name: "John", age: 30, tags: ["javascript", "typescript"], // Arrays become multiple entries avatar: fileBlob, // Files and blobs handled correctly settings: { theme: "dark" }, // Objects are JSON stringified }); ``` toQueryString [#toquerystring] Converts objects to URL query strings: ```ts title="query-string.ts" import { toQueryString } from "@zayne-labs/callapi/utils"; const query = toQueryString({ page: 1, tags: ["js", "ts"], // Arrays become multiple params search: "javascript", active: null, // Null/undefined values are skipped }); // Result: "page=1&tags=js&tags=ts&search=javascript" ``` Error Handling Utilities [#error-handling-utilities] Error Type Guards [#error-type-guards] Check error types in result objects: ```ts title="error-guards.ts" import { isHTTPError, isJavascriptError, isValidationError } from "@zayne-labs/callapi/utils"; function handleError(error: unknown) { if (isHTTPError(error)) { return `HTTP ${error.originalError.response.status}: ${error.message}`; } if (isValidationError(error)) { return `Validation failed: ${error.message}`; } // Any error that basically isn't the above two if (isJavascriptError(error)) { return `JavaScript error: ${error.message}`; } } ``` Error Instance Guards [#error-instance-guards] Check CallApi-specific error instances when using `throwOnError: true`: ```ts title="error-instances.ts" import { isHTTPErrorInstance, isValidationErrorInstance } from "@zayne-labs/callapi/utils"; try { const { data } = await apiClient("/users", { throwOnError: true }); } catch (error) { if (isHTTPErrorInstance(error)) { console.error(`HTTP ${error.response.status}: ${error.message}`); } if (isValidationErrorInstance(error)) { console.error(`Validation failed: ${error.message}`); } } ``` Constants [#constants] fallBackRouteSchemaKey [#fallbackrouteschemakey] Constant for defining fallback schemas: ```ts title="fallback-schema.ts" import { fallBackRouteSchemaKey } from "@zayne-labs/callapi/constants"; import { defineSchema } from "@zayne-labs/callapi/utils"; import { z } from "zod"; const schema = defineSchema({ [fallBackRouteSchemaKey]: { headers: z.object({ "x-api-key": z.string(), }), }, }); ``` # CallApi: Type Helpers URL: /docs/utilities/type-helpers Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/utilities/type-helpers.mdx Advanced type helpers for CallApi CallApi provides type helpers for complex type scenarios and better type safety. Client Context Types [#client-context-types] GetCallApiContext [#getcallapicontext] Provides type safety and IntelliSense when defining context types: ```ts title="custom-context.ts" import type { GetCallApiContext } from "@zayne-labs/callapi"; type MyAppContext = GetCallApiContext<{ Data: { email: string; id: number; name: string; }; ErrorData: { code: string; details?: Record; message: string; }; Meta: { feature?: string; requestId: string; userId: string; }; ResultMode: "all"; }>; // Use with createFetchClientWithContext const createAppClient = createFetchClientWithContext(); ``` Benefits: validates context structure at compile time, provides autocomplete, and catches typos. createFetchClientWithContext [#createfetchclientwithcontext] Create clients with custom context types: ```ts title="typed-client.ts" import { createFetchClientWithContext, GetCallApiContext } from "@zayne-labs/callapi"; type AppContext = GetCallApiContext<{ Data: { id: number; name: string }; ErrorData: { code: string; message: string }; Meta: { requestId: string; userId: string }; }>; const callBackendApi = createFetchClientWithContext()({ baseURL: "https://api.example.com", meta: { userId: "123", // Fully typed based on AppContext.Meta requestId: "req-456", }, }); const { data } = await callBackendApi("/users/1"); // data is typed as AppContext.Data: { id: number; name: string } ``` Global Meta Registration [#global-meta-registration] Register global meta types using module augmentation: ```ts title="types/callapi.d.ts" declare module "@zayne-labs/callapi" { interface Register { meta: { feature?: string; priority?: "high" | "low" | "normal"; requestId?: string; source?: "background" | "system" | "user-action"; userId?: string; }; } } ``` Hook Context Types [#hook-context-types] Each hook has a corresponding context type: ```ts title="hook-types.ts" import type { ErrorContext, RequestContext, SuccessContext } from "@zayne-labs/callapi"; function createLoggingHooks() { return { onRequest: (ctx: RequestContext) => { console.log(`Request: ${ctx.options.initURL}`); }, onSuccess: (ctx: SuccessContext<{ Data: TData }>) => { console.log(`Success: ${ctx.data}`); }, onError: (ctx: ErrorContext<{ ErrorData: TErrorData }>) => { console.log(`Error: ${ctx.error.message}`); }, }; } ``` Plugin Types [#plugin-types] CallApiPlugin [#callapiplugin] Type for defining plugins: ```ts title="plugin-types.ts" import type { CallApiPlugin } from "@zayne-labs/callapi"; import { z } from "zod"; const myOptionsSchema = z.object({ apiKey: z.string(), debug: z.boolean().default(false), }); type MyPluginContext = GetCallApiContext<{ InferredExtraOptions: typeof myOptionsSchema; }>; const myPlugin = { id: "my-plugin", name: "My Plugin", setup: ({ options }) => { console.log(`API Key: ${options.apiKey}, Debug: ${options.debug}`); return {}; }, hooks: { onRequest: ({ options }) => { if (options.debug) { console.log("Debug mode enabled"); } }, }, } satisfies CallApiPlugin; ``` These type utilities provide advanced TypeScript support while maintaining type safety throughout your application. # CallApi: Logger URL: /docs/utilities/plugins/logger Source: https://raw.githubusercontent.com/zayne-labs/callapi/refs/heads/main/apps/docs/content/docs/utilities/plugins/logger.mdx Comprehensive HTTP request/response logging with beautiful console output The logger plugin provides detailed, structured logging for all HTTP request/response lifecycle events. Built on [consola](https://github.com/unjs/consola). Features [#features] * Logs all HTTP requests and responses * Tracks errors and retries * Color-coded console output * Customizable logging options Installation [#installation] npm pnpm yarn bun ```bash npm install @zayne-labs/callapi-plugins ``` ```bash pnpm add @zayne-labs/callapi-plugins ``` ```bash yarn add @zayne-labs/callapi-plugins ``` ```bash bun add @zayne-labs/callapi-plugins ``` Quick Start [#quick-start] ```ts title="api.ts" import { createFetchClient } from "@zayne-labs/callapi"; import { loggerPlugin } from "@zayne-labs/callapi-plugins"; const callMainApi = createFetchClient({ baseURL: "https://api.example.com", plugins: [ loggerPlugin({ enabled: process.env.NODE_ENV === "development", mode: process.env.DEBUG === "true" ? "verbose" : "basic", }), ], }); ``` Configuration [#configuration] enabled [#enabled] Type: `boolean | { onError?: boolean; onRequest?: boolean; onRequestError?: boolean; onResponse?: boolean; onResponseError?: boolean; onRetry?: boolean; onSuccess?: boolean; onValidationError?: boolean; }` Default: `true` Toggle logging on/off. Can be a boolean for all logging, or an object for granular control over specific events. ```ts // Simple boolean toggle export const client1 = createFetchClient({ plugins: [ loggerPlugin({ enabled: process.env.NODE_ENV === "development", }), ], }); // Granular control export const client2 = createFetchClient({ plugins: [ loggerPlugin({ enabled: { onRequest: true, onSuccess: true, onError: true, // Enable all error logging by default onValidationError: false, // Disable validation error logging specifically }, }), ], }); // Alternative: Enable only specific error types export const client3 = createFetchClient({ plugins: [ loggerPlugin({ enabled: { onRequest: true, onSuccess: true, onRequestError: true, // Only network/request errors onResponseError: false, // Disable HTTP error logging onValidationError: false, }, }), ], }); ``` consoleObject [#consoleobject] Type: `ConsoleLikeObject` Default: A pre-configured `consola` instance Provide a custom console-like object implementing this interface: ```ts interface ConsoleLikeObject { error: (...args: any[]) => void; fail?: (...args: any[]) => void; log: (...args: any[]) => void; success?: (...args: any[]) => void; warn?: (...args: any[]) => void; } ``` Example: ```ts title="api.ts" import { loggerPlugin, type ConsoleLikeObject } from "@zayne-labs/callapi-plugins"; const customLogger: ConsoleLikeObject = { log: (...args) => console.log("[API]", ...args), error: (...args) => console.error("[API ERROR]", ...args), warn: (...args) => console.warn("[API WARN]", ...args), success: (...args) => console.log("[API SUCCESS]", ...args), }; export const client = createFetchClient({ plugins: [loggerPlugin({ consoleObject: customLogger })], }); ``` mode [#mode] Type: `"basic" | "verbose"` Default: `"basic"` Controls the verbosity of logging output: * **`"basic"`**: Standard logging with essential information * **`"verbose"`**: Detailed debugging with full payloads, error data, and additional context ```ts export const client = createFetchClient({ plugins: [loggerPlugin({ mode: "verbose" })], }); ``` Logged Events [#logged-events] The logger plugin logs these events: * **Request Started**: When a request is initiated * **Request Error**: When a request fails to be sent * **Response Error**: When a response has an error status * **Retry**: When a request is being retried (with attempt count) * **Success**: When a request completes successfully * **Validation Error**: When request/response validation fails (via schema validation)