Middlewares
Intercept and wrap internal operations in CallApi
Middleware lets you wrap and intercept internal operations in CallApi. Currently, only fetch middleware is available for intercepting requests at the network layer, but the system is designed to support additional middleware types in the future.
When a given middleware is defined at multiple levels (plugins, base config, per-request), they compose automatically (i.e. each one wrapping the next in a chain).
Fetch Middleware
Fetch middleware wraps the underlying fetch implementation (native fetch or your custom implementation via customFetchImpl), giving you full control over when and how requests execute.
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. Use this to:
- Cache responses
- Track upload/download progress
- Handle offline mode
- Add logging or metrics
- Short-circuit requests
- Modify requests/responses
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 reverse order, wrapping each other like onion layers:
Execution flow: Instance → Base → Plugins (reverse order) → 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
The key difference: middleware controls execution flow, hooks observe and react.
Use fetch middleware when you need to:
- Control whether fetch is called - Short-circuit requests, return cached responses without hitting the network
- Replace the fetch implementation - Use XHR, mock responses, or alternative HTTP clients
- Transform the response - Return a completely different Response object
- Wrap the network call - Add retry logic, circuit breakers, or request queuing at the fetch level
Use hooks when you need to:
- Observe lifecycle events - React to what's happening without controlling the flow
- Log or track metrics - Record timing, errors, or success rates
- Modify request options - Change headers, body, or other options before the request (by mutation)
- Handle errors - Process errors after they occur
- Side effects - Update UI, trigger analytics, or perform other actions
Key distinction: Hooks receive context and can mutate objects, but their return values are ignored. Middleware wraps functions and controls what gets called and what gets returned. If you need to prevent a fetch call or return a custom response, use middleware. If you need to observe or react to events, use hooks.
Last updated on