Plugin Structure
A CallApi plugin has this structure:import { definePlugin } from "@zayne-labs/callapi/utils";
const myPlugin = definePlugin({
// Required
id: "unique-plugin-id",
name: "My Plugin",
// Optional
version: "1.0.0",
description: "What this plugin does",
// Plugin functionality
setup: (context) => { /* ... */ },
hooks: { /* ... */ },
middlewares: { /* ... */ },
defineExtraOptions: () => { /* ... */ },
});
Authentication Plugin
A plugin that automatically adds authentication headers:import { definePlugin } from "@zayne-labs/callapi/utils";
import { z } from "zod";
const authSchema = z.object({
skipAuth: z.boolean().optional(),
});
export const authPlugin = definePlugin({
id: "auth",
name: "Authentication Plugin",
version: "1.0.0",
defineExtraOptions: () => authSchema,
setup: async (ctx) => {
// Skip auth if requested
if (ctx.options.skipAuth) {
return;
}
// Get token from storage or auth service
const token = await getAuthToken();
if (!token) {
return; // No token available
}
return {
request: {
...ctx.request,
headers: {
...ctx.request.headers,
Authorization: `Bearer ${token}`,
},
},
};
},
hooks: {
onResponseError: async (ctx) => {
// Auto-refresh token on 401
if (ctx.response.status === 401) {
await refreshAuthToken();
}
},
},
});
// Usage
const api = createFetchClient({
baseURL: "https://api.example.com",
plugins: [authPlugin],
});
// With auth
const { data: users } = await api("/users");
// Skip auth for specific request
const { data: publicData } = await api("/public", {
skipAuth: true,
});
Retry Plugin
A plugin that retries failed requests:import { definePlugin } from "@zayne-labs/callapi/utils";
import { z } from "zod";
const retrySchema = z.object({
maxRetries: z.number().int().positive().optional(),
retryDelay: z.number().positive().optional(),
retryOn: z.array(z.number()).optional(),
});
export const retryPlugin = (options?: {
maxRetries?: number;
retryDelay?: number;
retryOn?: number[];
}) => {
const config = {
maxRetries: options?.maxRetries ?? 3,
retryDelay: options?.retryDelay ?? 1000,
retryOn: options?.retryOn ?? [408, 429, 500, 502, 503, 504],
};
return definePlugin({
id: "retry",
name: "Retry Plugin",
version: "1.0.0",
defineExtraOptions: () => retrySchema,
middlewares: (context) => {
return {
fetchMiddleware: (ctx) => async (input, init) => {
const maxRetries = context.options.maxRetries ?? config.maxRetries;
const retryDelay = context.options.retryDelay ?? config.retryDelay;
const retryOn = context.options.retryOn ?? config.retryOn;
let lastError: Error | undefined;
let lastResponse: Response | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await ctx.fetchImpl(input, init);
// Return if successful or status not in retry list
if (response.ok || !retryOn.includes(response.status)) {
return response;
}
lastResponse = response;
// Wait before retrying
if (attempt < maxRetries - 1) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay * (attempt + 1))
);
}
} catch (error) {
lastError = error as Error;
// Wait before retrying
if (attempt < maxRetries - 1) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay * (attempt + 1))
);
}
}
}
// All retries exhausted
if (lastResponse) {
return lastResponse;
}
throw lastError ?? new Error("Max retries reached");
},
};
},
});
};
// Usage
const api = createFetchClient({
plugins: [retryPlugin({ maxRetries: 5, retryDelay: 2000 })],
});
// Override per request
const { data } = await api("/users", {
maxRetries: 10,
});
Caching Plugin
A plugin that caches GET requests:import { definePlugin } from "@zayne-labs/callapi/utils";
import { z } from "zod";
const cacheSchema = z.object({
cache: z.enum(["default", "no-cache", "force-cache"]).optional(),
cacheTime: z.number().positive().optional(),
});
interface CacheEntry {
response: Response;
timestamp: number;
}
export const cachePlugin = (options?: {
defaultCacheTime?: number;
}) => {
const cache = new Map<string, CacheEntry>();
const defaultCacheTime = options?.defaultCacheTime ?? 5 * 60 * 1000; // 5 minutes
return definePlugin({
id: "cache",
name: "Caching Plugin",
version: "1.0.0",
defineExtraOptions: () => cacheSchema,
middlewares: (context) => {
return {
fetchMiddleware: (ctx) => async (input, init) => {
const cacheOption = context.options.cache ?? "default";
const cacheTime = context.options.cacheTime ?? defaultCacheTime;
// Skip cache for non-GET requests
const method = init?.method || "GET";
if (method !== "GET") {
return ctx.fetchImpl(input, init);
}
const cacheKey = input.toString();
// Force cache bypass
if (cacheOption === "no-cache") {
cache.delete(cacheKey);
return ctx.fetchImpl(input, init);
}
// Check cache
const cached = cache.get(cacheKey);
if (cached) {
const age = Date.now() - cached.timestamp;
// Return cached if not expired or force-cache
if (cacheOption === "force-cache" || age < cacheTime) {
return cached.response.clone();
}
}
// Make request
const response = await ctx.fetchImpl(input, init);
// Cache successful responses
if (response.ok) {
cache.set(cacheKey, {
response: response.clone(),
timestamp: Date.now(),
});
}
return response;
},
};
},
});
};
// Usage
const api = createFetchClient({
plugins: [cachePlugin({ defaultCacheTime: 10 * 60 * 1000 })],
});
// Use cache (default)
const { data: users1 } = await api("/users");
// Use cached response
const { data: users2 } = await api("/users");
// Skip cache
const { data: freshUsers } = await api("/users", {
cache: "no-cache",
});
Rate Limiting Plugin
A plugin that implements client-side rate limiting:import { definePlugin } from "@zayne-labs/callapi/utils";
interface RateLimitConfig {
maxRequests: number;
perMilliseconds: number;
}
export const rateLimitPlugin = (config: RateLimitConfig) => {
const queue: Array<() => void> = [];
const timestamps: number[] = [];
const checkRateLimit = async () => {
const now = Date.now();
const cutoff = now - config.perMilliseconds;
// Remove old timestamps
while (timestamps.length > 0 && timestamps[0]! < cutoff) {
timestamps.shift();
}
// If under limit, allow immediately
if (timestamps.length < config.maxRequests) {
timestamps.push(now);
return;
}
// Wait until we can make another request
const oldestTimestamp = timestamps[0]!;
const waitTime = oldestTimestamp + config.perMilliseconds - now;
await new Promise((resolve) => setTimeout(resolve, waitTime));
// Try again
return checkRateLimit();
};
return definePlugin({
id: "rate-limit",
name: "Rate Limiting Plugin",
version: "1.0.0",
middlewares: {
fetchMiddleware: (ctx) => async (input, init) => {
// Wait for rate limit
await checkRateLimit();
// Make request
return ctx.fetchImpl(input, init);
},
},
});
};
// Usage - limit to 10 requests per second
const api = createFetchClient({
plugins: [
rateLimitPlugin({
maxRequests: 10,
perMilliseconds: 1000,
}),
],
});
Request ID Plugin
A plugin that adds unique request IDs:import { definePlugin } from "@zayne-labs/callapi/utils";
import { v4 as uuidv4 } from "uuid";
export const requestIdPlugin = definePlugin({
id: "request-id",
name: "Request ID Plugin",
version: "1.0.0",
setup: (ctx) => {
const requestId = uuidv4();
return {
request: {
...ctx.request,
headers: {
...ctx.request.headers,
"X-Request-ID": requestId,
},
},
options: {
...ctx.options,
meta: {
...ctx.options.meta,
requestId,
},
},
};
},
hooks: {
onRequest: (ctx) => {
console.log(`[${ctx.options.meta?.requestId}] → ${ctx.request.method} ${ctx.options.fullURL}`);
},
onSuccess: (ctx) => {
console.log(`[${ctx.options.meta?.requestId}] ✓ Success`);
},
onError: (ctx) => {
console.error(`[${ctx.options.meta?.requestId}] ✗ Error`, ctx.error);
},
},
});
Performance Monitoring Plugin
import { definePlugin } from "@zayne-labs/callapi/utils";
interface PerformanceMetrics {
url: string;
method: string;
duration: number;
status?: number;
success: boolean;
}
export const performancePlugin = (options?: {
onMetric?: (metric: PerformanceMetrics) => void;
}) => {
return definePlugin({
id: "performance",
name: "Performance Monitoring Plugin",
version: "1.0.0",
setup: (ctx) => {
return {
options: {
...ctx.options,
meta: {
...ctx.options.meta,
startTime: performance.now(),
},
},
};
},
hooks: {
onSuccess: (ctx) => {
const duration = performance.now() - (ctx.options.meta?.startTime ?? 0);
const metric: PerformanceMetrics = {
url: ctx.options.fullURL,
method: ctx.request.method,
duration,
status: ctx.response.status,
success: true,
};
options?.onMetric?.(metric);
if (duration > 1000) {
console.warn(`Slow request: ${ctx.request.method} ${ctx.options.fullURL} took ${duration.toFixed(2)}ms`);
}
},
onError: (ctx) => {
const duration = performance.now() - (ctx.options.meta?.startTime ?? 0);
const metric: PerformanceMetrics = {
url: ctx.options.fullURL,
method: ctx.request.method,
duration,
success: false,
};
options?.onMetric?.(metric);
},
},
});
};
// Usage with custom metrics handler
const metrics: PerformanceMetrics[] = [];
const api = createFetchClient({
plugins: [
performancePlugin({
onMetric: (metric) => {
metrics.push(metric);
// Send to analytics service
analytics.track("api_request", metric);
},
}),
],
});
Combining Multiple Plugins
import { createFetchClient } from "@zayne-labs/callapi";
import { loggerPlugin } from "@zayne-labs/callapi-plugins";
import {
authPlugin,
retryPlugin,
cachePlugin,
requestIdPlugin,
performancePlugin,
} from "./plugins";
const api = createFetchClient({
baseURL: "https://api.example.com",
plugins: [
requestIdPlugin,
authPlugin,
retryPlugin({ maxRetries: 3 }),
cachePlugin({ defaultCacheTime: 5 * 60 * 1000 }),
performancePlugin(),
loggerPlugin({ mode: "verbose" }),
],
});
See Also
- definePlugin - Plugin creation utility
- Logger Plugin - Built-in logger plugin
- Plugins Guide - Detailed plugin documentation
- Hooks - Available lifecycle hooks
- Middlewares - Working with middleware