Skip to content
TypeWire

@tahanabavi/typefetch

documents v1.10.0

The typed HTTP client every other package is built around.

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:

ts
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.

ts
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 later

requestId 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.

ts
client.instrument({
resolveOverride(endpointId, input) {
if (endpointId === "user.getUser") {
return { mock: { id: "forced", name: "Forced User" } };
}
},
});
ts
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
};
  • mock bypasses the network regardless of mock mode and is still validated against the (possibly overridden) response schema.

  • error throws a RichError and fires onError, like a real failing endpoint.

  • latencyMs is awaited before the request resolves.

  • request / response swap 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.

ts
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:

ts
client.setMockMode(true, {
min: 200,
max: 1000,
});

Dynamic mock data is also supported:

ts
mockData: () => ({
id: crypto.randomUUID(),
name: "Dynamic Mock User",
});

Mock responses are still validated against the endpoint response schema.