Instrumentation & Runtime Overrides
Beyond middleware (which only sees the raw Response), TypeFetch exposes an
optional instrumentation layer that reports the parsed input and parsed
output of every request and can override a request at runtime. It's designed for
tooling — a devtools/inspector or a query layer — and is fully additive: with no
hook registered, request handling is unchanged.
Endpoint metadata
Every generated method carries stable, read-only metadata:
const api = client.modules;
api.user.getUser.endpointId; // "user.getUser"
api.user.getUser.endpoint; // the original contract def (schemas, method, path, ...)Lifecycle events
Register a hook with client.instrument(...); it returns an unsubscribe
function.
const stop = client.instrument({
on(event) {
// event.type: "start" | "success" | "error" | "progress"
// start: { requestId, endpointId, method, url, input, timestamp }
// success: { requestId, endpointId, data, durationMs, fromMock }
// error: { requestId, endpointId, status?, error, durationMs }
// progress: { requestId, endpointId, phase, loaded, total?, percent?,
// lengthComputable, durationMs }
console.log(event.type, event.endpointId);
},
});
stop(); // detach laterrequestId correlates a request's start with its success/error.
progress events are emitted only for requests that were given an
onUploadProgress / onDownloadProgress handler. Attaching instrumentation
never causes progress tracking on its own — opening devtools must not change
which transport a request uses or re-stream a body nobody asked to measure.
Runtime overrides
A hook can resolve a per-request Override to change what a single request does
without mutating the contract. The first hook to return an override wins.
client.instrument({
resolveOverride(endpointId, input) {
if (endpointId === "user.getUser") {
return { mock: { id: "forced", name: "Forced User" } };
}
},
});type Override = {
mock?: unknown | ((input: unknown) => unknown); // force mock, bypass network
error?: { status?: number; code?: string; message?: string; body?: unknown }; // simulate failure
latencyMs?: number; // inject latency
request?: z.ZodTypeAny; // swap request schema at runtime
response?: z.ZodTypeAny; // swap response schema at runtime
};mockbypasses the network regardless of mock mode and is still validated against the (possibly overridden) response schema.errorthrows aRichErrorand firesonError, like a real failing endpoint.latencyMsis awaited before the request resolves.request/responseswap the validation schema for that request only, so you can test structural changes at runtime.
Fields are independent and compose. Full details in
docs/releases/v1.7.0.md.
Mock Mode
Mock mode lets you return endpoint-level mock data instead of calling the network.
const contracts = {
user: {
getUser: {
method: "GET",
path: "/users/:id",
request: z.object({
path: z.object({
id: z.string(),
}),
}),
response: z.object({
id: z.string(),
name: z.string(),
}),
mockData: {
id: "mock-1",
name: "Mock User",
},
},
},
} as const;Enable mock mode:
client.setMockMode(true, {
min: 200,
max: 1000,
});Dynamic mock data is also supported:
mockData: () => ({
id: crypto.randomUUID(),
name: "Dynamic Mock User",
});Mock responses are still validated against the endpoint response schema.