Overview
The CallApiPlugin interface allows you to create reusable extensions for CallApi. Plugins can add hooks, middlewares, extra options, and schemas to modify request/response behavior across all requests.
Type Definition
interface CallApiPlugin<TCallApiContext extends CallApiContext = DefaultCallApiContext> {
id: string;
name: string;
version?: string;
description?: string;
defineExtraOptions?: () => TCallApiContext['InferredExtraOptions'];
schema?: BaseCallApiSchemaAndConfig;
setup?: (context: PluginSetupContext) => Awaitable<PluginInitResult | void>;
hooks?: PluginHooks | ((context: PluginSetupContext) => Awaitable<PluginHooks | void>);
middlewares?: PluginMiddlewares | ((context: PluginSetupContext) => Awaitable<PluginMiddlewares | void>);
}
Required Properties
A unique identifier for the plugin. Used to identify and deduplicate plugins.
A human-readable name for the plugin.
Optional Properties
Version string for the plugin.
A description of what the plugin does.description: 'Adds authentication and retry logic to all requests'
Defines additional options that users can pass to callApi when using this plugin.defineExtraOptions: () => ({
apiKey: undefined as string | undefined,
debug: false as boolean
})
Users can then use these options:await callApi('/users', {
apiKey: 'my-key',
debug: true
});
schema
BaseCallApiSchemaAndConfig
Base validation schemas for the plugin. Applies validation rules to requests/responses.import { z } from 'zod';
schema: {
routes: {
'/users': {
response: z.array(z.object({
id: z.string(),
name: z.string()
}))
}
}
}
Plugin Functions
setup
setup
(context: PluginSetupContext) => Awaitable<PluginInitResult | void>
Called when the plugin is initialized, before any other plugin functions.Can return modifications to the request, options, or URL:setup: async ({ initURL, request, options, config, baseConfig }) => {
// Modify the URL
const newURL = initURL + '?plugin=active';
// Modify request
const newRequest = {
...request,
headers: {
...request.headers,
'X-Plugin': 'active'
}
};
// Modify options
const newOptions = {
...options,
timeout: 10000
};
return {
initURL: newURL,
request: newRequest,
options: newOptions
};
}
hooks
hooks
PluginHooks | ((context: PluginSetupContext) => Awaitable<PluginHooks | void>)
Lifecycle hooks for the plugin. Can be a static object or a function that returns hooks.Static hooks:hooks: {
onRequest: ({ request }) => {
console.log('Plugin request hook');
},
onSuccess: ({ data }) => {
console.log('Plugin success hook:', data);
}
}
Dynamic hooks:hooks: ({ options }) => {
if (!options.meta?.enablePlugin) {
return; // No hooks if plugin disabled
}
return {
onRequest: ({ request }) => {
console.log('Dynamic plugin hook');
}
};
}
middlewares
middlewares
PluginMiddlewares | ((context: PluginSetupContext) => Awaitable<PluginMiddlewares | void>)
Fetch middlewares for the plugin. Can be a static object or a function that returns middlewares.middlewares: {
fetchMiddleware: ({ fetchImpl }) => async (input, init) => {
console.log('Plugin middleware intercept');
const response = await fetchImpl(input, init);
return response;
}
}
Plugin Setup Context
The setup context provides access to the current request configuration:
The initial URL for the request.
baseConfig
BaseCallApiConfig
required
Base configuration from createFetchClient.
Instance-specific configuration.
options
CallApiExtraOptions
required
Merged extra options.
request
CallApiRequestOptions
required
Request options (method, headers, body, etc.).
Plugin Init Result
The setup function can return modifications:
type PluginInitResult = {
initURL?: InitURLOrURLObject;
request?: Partial<CallApiRequestOptions>;
options?: Partial<CallApiExtraOptions>;
baseConfig?: Partial<BaseCallApiConfig>;
config?: Partial<CallApiConfig>;
}
Examples
Simple Logger Plugin
import type { CallApiPlugin } from 'callapi';
const loggerPlugin = (): CallApiPlugin => ({
id: 'logger',
name: 'Logger Plugin',
version: '1.0.0',
description: 'Logs all requests and responses',
hooks: {
onRequest: ({ request }) => {
console.log('→', request.method, request.url);
},
onResponse: ({ response }) => {
console.log('←', response.status, response.url);
},
onError: ({ error }) => {
console.error('✗', error.message);
}
}
});
// Usage
const api = createFetchClient({
baseURL: 'https://api.example.com',
plugins: [loggerPlugin()]
});
API Key Plugin
interface ApiKeyPluginOptions {
apiKey: string;
headerName?: string;
}
const apiKeyPlugin = (options: ApiKeyPluginOptions): CallApiPlugin => ({
id: 'api-key',
name: 'API Key Plugin',
description: 'Adds API key to request headers',
defineExtraOptions: () => ({
apiKey: undefined as string | undefined
}),
hooks: {
onRequest: ({ request, options: reqOptions }) => {
const apiKey = reqOptions.apiKey || options.apiKey;
const headerName = options.headerName || 'X-API-Key';
if (apiKey) {
request.headers[headerName] = apiKey;
}
}
}
});
// Usage
const api = createFetchClient({
baseURL: 'https://api.example.com',
plugins: [apiKeyPlugin({ apiKey: 'secret-key' })]
});
// Can override per request
await api('/users', { apiKey: 'different-key' });
Retry Plugin
interface RetryPluginOptions {
maxRetries?: number;
retryDelay?: number;
retryOn?: number[];
}
const retryPlugin = (options: RetryPluginOptions = {}): CallApiPlugin => {
const {
maxRetries = 3,
retryDelay = 1000,
retryOn = [408, 429, 500, 502, 503, 504]
} = options;
return {
id: 'retry',
name: 'Retry Plugin',
description: 'Automatically retries failed requests',
setup: () => ({
options: {
retry: maxRetries
}
}),
hooks: {
onRetry: async ({ error, retryAttemptCount, response }) => {
console.log(`Retry attempt ${retryAttemptCount}/${maxRetries}`);
// Don't retry if status not in retryOn list
if (response?.status && !retryOn.includes(response.status)) {
throw error; // Stop retrying
}
// Wait before retrying
const delay = retryDelay * Math.pow(2, retryAttemptCount - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
};
// Usage
const api = createFetchClient({
baseURL: 'https://api.example.com',
plugins: [retryPlugin({ maxRetries: 5 })]
});
Cache Plugin
interface CachePluginOptions {
ttl?: number; // Time to live in milliseconds
}
const cachePlugin = (options: CachePluginOptions = {}): CallApiPlugin => {
const { ttl = 60000 } = options; // 1 minute default
const cache = new Map<string, { data: any; timestamp: number }>();
return {
id: 'cache',
name: 'Cache Plugin',
description: 'Caches GET request responses',
middlewares: {
fetchMiddleware: ({ fetchImpl }) => async (input, init) => {
const url = input.toString();
const method = init?.method || 'GET';
// Only cache GET requests
if (method.toUpperCase() !== 'GET') {
return fetchImpl(input, init);
}
// Check cache
const cached = cache.get(url);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('Cache hit:', url);
return new Response(JSON.stringify(cached.data), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
// Fetch and cache
const response = await fetchImpl(input, init);
if (response.ok) {
const clone = response.clone();
const data = await clone.json();
cache.set(url, { data, timestamp: Date.now() });
}
return response;
}
}
};
};
// Usage
const api = createFetchClient({
baseURL: 'https://api.example.com',
plugins: [cachePlugin({ ttl: 120000 })] // 2 minute cache
});
Analytics Plugin
interface AnalyticsPluginOptions {
trackingId: string;
enabled?: boolean;
}
const analyticsPlugin = (options: AnalyticsPluginOptions): CallApiPlugin => ({
id: 'analytics',
name: 'Analytics Plugin',
description: 'Tracks API usage and performance',
hooks: ({ options: reqOptions }) => {
if (!options.enabled) return;
return {
onRequest: ({ request, options }) => {
options.meta = {
...options.meta,
analyticsStart: Date.now()
};
},
onSuccess: ({ data, response, options: reqOptions }) => {
const duration = Date.now() - reqOptions.meta?.analyticsStart;
analytics.track('api_success', {
trackingId: options.trackingId,
endpoint: response.url,
method: response.method,
status: response.status,
duration
});
},
onError: ({ error, response, options: reqOptions }) => {
const duration = Date.now() - reqOptions.meta?.analyticsStart;
analytics.track('api_error', {
trackingId: options.trackingId,
endpoint: response?.url,
error: error.name,
status: response?.status,
duration
});
}
};
}
});
// Usage
const api = createFetchClient({
baseURL: 'https://api.example.com',
plugins: [
analyticsPlugin({
trackingId: 'UA-12345',
enabled: true
})
]
});
Auth Plugin with Token Refresh
interface AuthPluginOptions {
getToken: () => string | Promise<string>;
refreshToken: () => Promise<string>;
shouldRefresh?: (response: Response) => boolean;
}
const authPlugin = (options: AuthPluginOptions): CallApiPlugin => {
let currentToken: string | null = null;
return {
id: 'auth',
name: 'Auth Plugin',
description: 'Handles authentication with automatic token refresh',
hooks: {
onRequest: async ({ request }) => {
if (!currentToken) {
currentToken = await options.getToken();
}
request.headers['Authorization'] = `Bearer ${currentToken}`;
},
onResponseError: async ({ response, error }) => {
const shouldRefresh = options.shouldRefresh?.(response) ?? response.status === 401;
if (shouldRefresh) {
console.log('Refreshing auth token...');
currentToken = await options.refreshToken();
}
}
}
};
};
// Usage
const api = createFetchClient({
baseURL: 'https://api.example.com',
plugins: [
authPlugin({
getToken: () => localStorage.getItem('token') || '',
refreshToken: async () => {
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include'
});
const { token } = await response.json();
localStorage.setItem('token', token);
return token;
}
})
]
});
Using Plugins
Base Plugins
Add plugins to the base client configuration:
const api = createFetchClient({
baseURL: 'https://api.example.com',
plugins: [
loggerPlugin(),
retryPlugin({ maxRetries: 3 }),
cachePlugin({ ttl: 60000 })
]
});
Instance Plugins
Add plugins to specific instances:
const adminApi = api.create({
plugins: ({ basePlugins }) => [
...basePlugins,
authPlugin({ apiKey: 'admin-key' })
]
});
See Also