Skip to content
TypeWire

@tahanabavi/typefetch

v2.0.0

A strongly typed TypeScript HTTP client powered by Zod contracts, middleware, retries, mock data, contract testing, CLI tooling, and report generation.

Features

  • End-to-end TypeScript inference from Zod schemas

  • Runtime request and response validation

  • Structured request model: { path, query, body, headers }

  • Automatic path parameter injection

  • Automatic query string generation

  • JSON and form-data request bodies

  • JSON, text, blob, arrayBuffer, formData, file, stream and raw response types

  • Upload progress (via XMLHttpRequest) and download progress

  • Middleware pipeline

  • Built-in retry engine with backoff strategies

  • Timeout and AbortController support

  • Static tokens and dynamic token providers

  • Mock mode for development and testing

  • Response wrapper support for API envelopes

  • Normalized error handling with RichError

  • Typed per-endpoint error responses via an optional errors map

  • Stable endpoint identifiers and contract metadata on every generated method

  • Runtime instrumentation with structured request lifecycle events

  • Per-request runtime overrides for mock, error, latency, and schema swapping

  • Pluggable transports — HTTP built in, gRPC and GraphQL as optional packages

  • Normalized error taxonomy (RichError.kind) shared across every transport

  • Zero runtime dependencies

  • Field-level encryption middleware (optional package)

  • Backward-compatible flat request schemas

  • Contract-driven API test runner

  • Automatic test input generation from Zod schemas

  • Schema, mock, live, and full API test modes

  • Markdown, HTML, and JSON test reports

  • CLI commands for project setup, endpoint listing, API testing, and release documentation (optional package)

  • Versioned release documentation under docs/releases


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",
},
},
},
}