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 the fetch call, and hook into 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 client 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, Response>();
return {
: () => async (, ) => {
const = .();
if (.()) {
return .()!.();
}
const = await .(, );
.(, .());
return ;
},
};
},
});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:
import { , type , type } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = .({
: .(["v1", "v2", "v3"]).(),
});
const = ({
: "api-version-plugin",
: "API Version Plugin",
: "Adds API version header to requests",
: "1.0.0",
: () => ,
: {
: () => {
const = .. ?? "v1";
.("API Version:", );
},
} satisfies <{
: typeof ;
}>,
: (: <{ : typeof }>) => {
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",
});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 section.
import { , type } from "@zayne-labs/callapi";
import { } from "zod";
const = .({
: .(["v1", "v2", "v3"]).(),
});
const = {
: "api-version-plugin",
: "API Version Plugin",
: "Adds API version header to requests",
: "1.0.0",
: () => ,
: {
: () => {
const = .. ?? "v1";
.("API Version:", );
},
},
// Look ma! No need to type the extra options individually for each section!
: () => {
const = .. ?? "v1";
return {
: {
....,
: { ....., "X-API-Version": },
},
};
},
} satisfies <{ : typeof }>;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