Plugins
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
Use the definePlugin helper for type-safe plugin creation:
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
const = ({
: "env-plugin",
: "Environment Plugin",
: "Adds environment-specific headers to requests",
: "1.0.0",
: ({ , , }) => {
const = .. || "development";
const = {
...,
: { ...., "X-Environment": },
};
const = {
...,
: {
....,
,
},
};
const = .("http://localhost:3000", "http://localhost:3001");
return {
: ,
: ,
: ,
};
},
});
const = ({
: "http://localhost:3000",
: [],
});Or alternatively, you can also use TypeScript's satisfies keyword together with CallApiPlugin type to achieve the same effect:
import type { } from "@zayne-labs/callapi";
const = {
: "env-plugin",
: "Environment Plugin",
: "Adds environment-specific headers to requests",
: "1.0.0",
: ({ }) => {
const = .. || "development";
const = {
...,
: { ...., "X-Environment": },
};
return {
: ,
};
},
} satisfies ;Using Plugins
Base Plugins
Add plugins when creating a callApi instance to apply them to all requests:
const callBackendApi = createFetchClient({
plugins: [
envPlugin, // Handle environment-specific configurations
loggingPlugin, // Log request/response details
],
});Per-Request Plugins
Add plugins to individual requests for specific calls:
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.
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
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.
import { } from "@zayne-labs/callapi/utils";
const = ({
: "env-plugin",
: "Environment Plugin",
: "A plugin that adds environment-specific headers to requests",
: "1.0.0",
: ({ }) => {
const = .. ?? "development";
const = . !== ? "browser" : "node";
const = Intl.().().;
const = {
...,
: {
....,
"X-Environment": ,
"X-Client-Platform": ,
"X-Client-Timezone": ,
},
};
return {
: ,
};
},
});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 for detailed information about available hooks.
import { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
const = ({
: "my-plugin",
: "My Plugin",
: "A plugin that does something",
: "1.0.0",
: {
: () => {
// Do something with context object
},
: () => {
// Do something with context object
},
// More hooks can be added here
},
});
const = ({
: "http://localhost:3000",
: [],
});Dynamic Hooks:
Hooks can also be a function that receives the plugin setup context:
import { } from "@zayne-labs/callapi/utils";
const = ({
: "dynamic-plugin",
: "Dynamic Plugin",
: () => {
const = .();
return {
: () => {
.(`Request to ${.} took ${.() - }ms`);
},
};
},
});When multiple plugins are registered, their setup functions and hooks execute in the order they appear
in the plugins array.
Middleware
Plugins can define middleware to wrap internal operations. Currently, CallApi supports fetch middleware for intercepting requests at the network layer. See Middleware 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:
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:
import type { } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
const = ({
: "caching",
: "Caching Plugin",
: "1.0.0",
: (: ) => {
const = new <string, >();
return {
: () => async (, ) => {
const = .();
if (.()) {
return .()!.();
}
const = await .(, );
.(, .());
return ;
},
};
},
});Defining Extra Options
Use extraOptionsHelper<T>() with definePlugin to add request options to a callApi instance that uses the plugin. If its hooks or setup need to read those values, use definePluginWithContext<T>() and put their types in the context.
Here's a plugin that adds an apiVersion option to automatically set the API version header:
import { , type } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
type = { ?: "v1" | "v2" | "v3" };
type = <{
: ;
: { ?: string };
}>;
const = <>()({
: "api-version-plugin",
: "API Version Plugin",
: "Adds API version header to requests",
: "1.0.0",
: {
: () => {
const = .. ?? "v1";
.("API Version:", );
.("Request ID:", ..?.);
},
},
: () => {
const = .. ?? "v1";
return {
: {
....,
: { ....., "X-API-Version": },
},
};
},
});
const = ({
: "https://api.example.com",
: [],
: "v2", // Default for all requests
});
// Use default v2
const { : } = await ("/users");
// Override to v3 for this request
const { : } = await ("/posts", {
: "v3",
: { : "posts-request" },
});
const = ();
await ("/reports", {
: [],
: "v3",
: { : "report-request" },
});The context lets the plugin read apiVersion and requestId in its callbacks. A callApi instance using the plugin accepts both values too.
If plugin callbacks don't need the custom types, use definePlugin with the helpers directly:
import { , , } from "@zayne-labs/callapi/utils";
const = ({
: "audit",
: "Audit",
: <{ ?: string }>(),
: <{ ?: { : string } }>(),
});Use these helpers when the plugin doesn't need to read the values itself. They add the types to calls made through a callApi instance using the plugin, but don't type its hooks or setup. If a tag and context define the same field, CallApi combines their types.
Install a plugin in createFetchClient to use it on every call. Pass it in a call's plugins option to use it only there, as oneOffApi does above.
Example: Metrics Plugin
Here's a complete example of a plugin that tracks API metrics:
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
Prop
Type
Last updated on