Skip to content
TypeWire

@tahanabavi/typefetch

documents v1.10.0

The typed HTTP client every other package is built around.

What's New in v2.0.0

Pluggable transports. TypeFetch spoke one wire; it now speaks any. gRPC and GraphQL ship as separate installable packages that feed the same contract, the same middleware chain, the same onError and the same devtools timeline — so application code stops caring which wire it is on.

ts
import { ApiClient } from "@tahanabavi/typefetch";
import { graphqlTransport } from "@tahanabavi/typefetch-graphql";
import { grpcTransport } from "@tahanabavi/typefetch-grpc";

const client = new ApiClient(
{
baseUrl: "https://api.example.com",
transports: [graphqlTransport(), grpcTransport()],
},
contracts,
);
ts
// http — unchanged; omitting `transport` means "http"
getUser: { method: "GET", path: "/users/:id", request, response },

// grpc
getUser: { transport: "grpc", service: "user.v1.UserService", rpc: "GetUser",
request, response },

// graphql — the selection set is generated from `response`
getUser: { transport: "graphql", operation: "query", root: "user",
request: z.object({ id: z.string() }), response: User },

transport: "grpc" does not compile until the package is installed: transports merge themselves into an open TransportRegistry, so installing one is what unlocks its contract shape — and its required fields are then fully checked.

Errors normalize across every wire. RichError.kind classifies every failure into one shared taxonomy, so a single handler covers all three protocols:

ts
client.onError((error) => {
if (error.kind === "unauthenticated") redirectToLogin(); // 401 · gRPC 16 · UNAUTHENTICATED
});

Zero runtime dependencies. crypto-js, node-forge and jiti moved to @tahanabavi/typefetch-encryption and @tahanabavi/typewire-cli.

Fixed: a request input that failed its own request schema used to escape as a raw ZodError — no kind, never passed to onError, invisible to instrumentation — while a bad response one line later did all four. Both ends of the contract now fail identically.

See docs/releases/v2.0.0.md for the full migration table and the behaviour changes to plan for.


What's New in v1.9.0

Two additions, both fully backward compatible.

1. Response types. An endpoint can declare how its body is decoded:

ts
downloadPdf: {
method: "GET",
path: "/reports/:id/pdf",
responseType: "blob", // ← new: json | text | blob | arrayBuffer
response: zBlob(), // | formData | file | stream | response
}

responseType: "file" goes one step further and hands back { blob, filename, contentType, size }, with filename already parsed out of Content-Disposition. See Response Types.

2. Upload and download progress, as per-request options:

ts
await api.media.upload(input, {
onUploadProgress: ({ percent }) => setProgress(percent ?? 0),
});

fetch has no upload-progress API, so passing onUploadProgress switches that request to XMLHttpRequest — middleware is unaffected, and requests without it keep the unchanged fetch path. In React, useMutation(endpoint, { trackProgress: "upload" }) puts it directly in the mutation's state. See Upload and Download Progress.

Two fixes, both behavior changes worth reading before upgrading:

  • Failed responses are no longer decoded with res.json() before their status is checked, so an HTML 502 or an empty 401 now produces a RichError carrying status instead of a raw SyntaxError. See Non-JSON error bodies.

  • onError now fires once per failed request instead of once per layer that saw the error — a request with maxRetries: 2 called it four times. See Error Handling.


What's New in v1.8.0

An endpoint may now carry an optional permission requirement, declared once on the contract — the flag names your frontend and backend both check:

ts
export const contracts = {
message: {
remove: {
method: "DELETE",
path: "/messages/:id",
permission: { require: ["chat.MANAGE_MESSAGES"] }, // ← new, optional
request: z.object({ path: z.object({ id: z.string() }) }),
response: z.object({ removed: z.string() }),
},
},
};

PermissionRequirement is { require?, any?, reason? }require needs all listed flags, any needs at least one. A server guard (@tahanabavi/typewire-nestjs) reads it off the contract and rejects with a typed 403 naming the missing flags; the client can read the same key to pre-block a call before it leaves the browser. The flag names reference a @tahanabavi/type-permission bit map, but the type is redeclared structurally, so TypeFetch gains no new dependency — exactly how type-devtools-core redeclares transport types.

Purely additive: endpoints without a permission key behave byte-for-byte as before. See docs/releases/v1.8.0.md.


What's New in v1.7.0

TypeFetch adds an additive instrumentation layer. Every generated method now carries stable metadata (endpointId + the original endpoint), and client.instrument(...) exposes structured lifecycle events (with parsed input and parsed output) plus optional per-request overrides — force a mock, simulate an error/latency, or swap the request/response schema at runtime, without touching the contract.

When no hook is registered, request handling is byte-for-byte identical to before. See Instrumentation & Runtime Overrides and docs/releases/v1.7.0.md.


What's New in v1.6.0

TypeFetch now includes a contract-driven testing layer and CLI tooling.

Testing runner

The testing runner can discover endpoints from your contracts, generate valid request inputs, run schema/mock/live checks, validate responses, and export reports.

Supported modes:

  • schema — validates generated or custom request input without network calls

  • mock — validates endpoint mockData against the response schema

  • live — executes real requests through ApiClient

  • full — runs schema, mock, and live phases where applicable

CLI

The CLI provides a simple workflow for setting up and running contract tests. It ships as a separate dev dependency, so a production bundle never carries it:

bash
npm install -D @tahanabavi/typewire-cli

Moved in v2.0.0. The CLI bin used to live in the core package (as typefetch; the command is now typewire), which meant every consumer installed jiti to get a client library.

bash
npx typewire init
npx typewire test --mode full --format markdown,json,html --output ./typefetch-report/report
npx typewire list

Endpoint test metadata

Endpoints can now include optional test metadata for custom inputs, tags, expected errors, destructive endpoint safety, and context-based flows.

ts
getUserById: {
method: "GET",
path: "/users/:id",
request: z.object({
path: z.object({
id: z.string(),
}),
}),
response: z.object({
id: z.string(),
name: z.string(),
}),
test: {
tags: ["user", "smoke"],
input: {
path: {
id: "user-1",
},
},
},
}

Versioned Release Documentation

Starting with v1.6.0, detailed documentation for larger updates is stored separately under docs/releases.

The main README stays focused on quick usage and core concepts, while release files contain deeper implementation notes, migration details, CLI examples, test strategy, and full feature explanations.

Recommended structure:

txt
docs/
releases/
v1.6.0.md
v1.6.7.md
v1.7.0.md
v1.8.0.md
v1.9.0.md
v2.0.0.md

The v2.0.0 document carries the full migration table for pluggable transports and the package split — read it before upgrading.

Notes

  • Always call client.init() before using client.modules.

  • All request inputs are validated with Zod.

  • All successful responses are validated with Zod.

  • Structured request schemas are recommended for new APIs.

  • Flat request schemas are still supported for backward compatibility.

  • GET requests do not send a body.

  • form-data endpoints should use bodyType: "form-data".

  • Auth tokens are only required for endpoints with auth: true.

  • Mock data bypasses network calls but still validates responses.

  • Every generated method exposes endpointId and endpoint metadata.

  • Instrumentation is opt-in; with no hook registered, request handling is unchanged.

  • Runtime overrides change a single request without mutating the contract.

  • Use npx typewire test --mode schema for fast contract validation.

  • Keep detailed release documentation in docs/releases.


License

MIT