Middlewares
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 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 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.
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
Base Config
Apply fetch middleware to all requests by adding it to your client configuration:
import { } from "@zayne-labs/callapi";
const = ({
: "https://api.example.com",
: () => async (, ) => {
.("Request:", );
const = await .(, );
.("Response:", .);
return ;
},
});Per-Request
Add fetch middleware to individual requests for one-off modifications:
import { } from "@zayne-labs/callapi";
const { , } = await ("/users", {
: () => async (, ) => {
const = .();
const = await .(, );
.(`Took ${.() - }ms`);
return ;
},
});In Plugins
Plugins can define fetch middleware to add reusable functionality. See Plugins for details.
import { } from "@zayne-labs/callapi/utils";
const = ({
: "logging",
: "Logging Plugin",
: {
: () => async (, ) => {
.("→", ?. || "GET", );
const = await .(, );
.("←", ., );
return ;
},
},
});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
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 → fetchThis reverse composition means the last middleware added gets the first chance to intercept the request.
Examples
Response Caching
import { , type } from "@zayne-labs/callapi";
import { } from "@zayne-labs/callapi/utils";
import { } from "zod";
const = .({
: .().().(),
: .(["cache-first", "no-cache"]).(),
});
export const = () => {
const = new <string, { : Response; : number }>();
return ({
: "caching-plugin",
: "Caching Plugin",
: () => ,
: ({
,
}: <{ : typeof }>) => {
const { = 60_000, = "cache-first" } = ;
return {
: () => async (, ) => {
if ( === "no-cache") {
return .(, );
}
const = instanceof ? . : .();
const = .();
const = async () => {
const = await .(, );
.(, { : .(), : .() });
return ;
};
if (!) {
.(`[Caching Plugin] Cache miss: ${}`);
return ();
}
const = .() - . > ;
if () {
.(`[Caching Plugin] Cache miss (expired): ${}`);
.();
return ();
}
.(`[Caching Plugin] Cache hit: ${}`);
return ..();
},
};
},
});
};
const = ({
: "https://api.example.com",
: [()],
: "cache-first",
: 2 * 60 * 1000, // 2 minutes
});
await ("/users");
await ("/users/:id", {
: "no-cache", // Skip cache for this request
});Offline Detection
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
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
Middleware can catch and handle errors or transform them before they reach your application:
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
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
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.
Last updated on