wiretype
The docs lie. The wire doesn't.
Record real API traffic and generate TypeScript types, zod schemas, MSW mocks, and OpenAPI 3.1 specs — from what your backend actually returns.
English | 한국어
Every frontend team knows this pain: the API docs say avatarUrl: string, but production returns null. The spec swears role is a free string, but it's always one of three values. Your MSW mock data drifted from the real server months ago, and nobody noticed until the demo broke.
wiretype fixes this by going to the source of truth — the actual bytes on the wire. Put its recording proxy in front of your dev API (or drop the Vite plugin into your dev server), use your app like you normally would, and generate:
wiretype-generated/
types.ts TypeScript interfaces for every endpoint (params / query / request / response)
schemas.ts zod schemas + z.infer type aliases
handlers.ts MSW v2 handlers seeded with real captured responses
openapi.json OpenAPI 3.1 spec
model.json the raw observed model — input for `wiretype diff` (drift detection)
Quickstart
npm i -D wiretype
Option A — standalone proxy (works with anything):
# 1. Put the recording proxy in front of your API
npx wiretype record --target http://localhost:8080 --port 5050
# 2. Point your app at :5050 and click around. Every request is recorded.
# 3. Generate all four outputs
npx wiretype gen
# 4. Explore recordings, inferred types, and generated code in a dashboard
npx wiretype ui
Option B — Vite plugin (zero workflow change):
// vite.config.ts — replaces your server.proxy entry for these prefixes
import wiretypeRecorder from 'wiretype/vite';
export default defineConfig({
plugins: [
wiretypeRecorder({
target: 'http://localhost:8080',
prefixes: ['/api'],
}),
],
});
vite --mode record # recording on; plain `vite` leaves the plugin inert
Leave the plugin in the array permanently — it only records when the dev
server runs in mode record (or the WIRETYPE env var is set; pass
enabled to override). Develop as usual, then npx wiretype gen. Your MSW
handlers are now guaranteed to match the real server.
What gets inferred
wiretype merges every observed sample per endpoint, so the more you click, the more accurate it gets:
| Feature | Example |
|---|---|
| Optional fields | key missing in some samples → avatarUrl?: string |
| Nullable | lastLoginAt: string | null |
| Enums (conservative) | repeated token-like strings only → role: "admin" | "editor" | "viewer" |
| String formats | uuid, date-time, date, email, uri → z.string().uuid(), .datetime(), ... |
| Integer vs float | z.number().int() |
| URL normalization | /api/users/42, /api/users/7 → GET /api/users/:userId (numeric / uuid / hash segments) |
| Per-status responses | 200 and 404 become separate types (GetUserResponse, GetUserResponse404) |
| Query params | ?page=2&limit=10 → { page: number; limit: number } |
Enum detection is deliberately conservative: only token-like strings (admin, in_progress) that actually repeat across 4+ samples become literal unions — so ids and titles never get frozen into enums.
Example output
// types.ts — generated by wiretype
export interface GetApiUsersByUserIdResponse {
/** format: uuid */ id: string;
name: string;
role: "admin" | "editor" | "viewer";
/** format: date-time */ createdAt: string;
lastLoginAt: string | null;
avatarUrl?: string;
}
export interface ApiEndpoints {
"GET /api/users/:userId": {
params: GetApiUsersByUserIdParams;
query: never;
request: never;
response: GetApiUsersByUserIdResponse;
};
// ...every recorded endpoint
}
// handlers.ts — MSW v2, seeded with the real response your server sent
export const handlers = [
http.get('*/api/users/:userId', () =>
HttpResponse.json({ id: '2b8f0a3e-...', name: 'Ada Lovelace', role: 'admin', ... }, { status: 200 }),
),
];
Prefer mock data out of handler code? wiretype gen --msw-fixtures writes each
body to fixtures/<operationId>.<status>.json and emits a thin handlers.ts
that imports them — re-recording refreshes the JSON without ever touching
handler code. (JSON imports may need resolveJsonModule: true in your tsconfig.)
CLI
wiretype record --target <url> [--port 5050] [--name session] [--dir .wiretype]
[--include <prefix...>] [--exclude <prefix...>]
wiretype gen [--name session] [--dir .wiretype] [--out wiretype-generated]
[--targets ts,zod,msw,openapi,model] [--msw-fixtures]
wiretype diff <a> <b> [--dir .wiretype] [--json] [--md] [--lang en|ko]
[--fail-on breaking|risky|info] [--ignore-unmatched]
wiretype list [--dir .wiretype]
wiretype ui [--dir .wiretype] [--port 5099]
wiretype ui serves a zero-dependency dark-theme dashboard: per-endpoint inferred type trees, raw request/response explorer, and all four generated outputs with copy buttons.
Vite plugin options
wiretypeRecorder({
target: 'http://localhost:8080', // upstream API base URL (required)
prefixes: ['/api'], // paths to intercept + record (required)
name: 'dev-session', // recording name (default "vite")
dir: '.wiretype', // store directory
enabled: true, // override auto-detect (default: mode "record" or WIRETYPE env)
excludePrefixes: ['/api/noisy'], // proxied but not recorded
maxBodyBytes: 1_048_576, // capture cap (bodies beyond are truncated)
redactHeaders: ['authorization'],// default: authorization, cookie, set-cookie, x-api-key
})
How it works
- Record — a zero-dependency reverse proxy (Node built-ins only) streams traffic through untouched, capturing method, path, query, headers (sensitive ones redacted), and JSON bodies. Gzip/brotli responses are forwarded raw and decoded only for capture.
- Infer — paths are normalized into patterns, then every JSON body per endpoint per status is merged into a shape AST: unions, optionals, nullability, formats, and enums fall out of the merge rules.
- Generate — four emitters render the same model into TypeScript, zod, MSW, and OpenAPI. Output is deterministic and compiles under
tsc --strict.
Schema drift detection
Recording once gives you types. Recording twice gives you a contract test.
wiretype diff compares two observed models (or a model against a committed
baseline) and grades every change:
$ wiretype diff v1 v2
wiretype diff — a: v1 (1 endpoints) vs b: v2 (2 endpoints)
1 breaking, 2 risky, 3 info · 1 compared, 0 only-in-a, 1 only-in-b
BREAKING (1)
breaking | field-removed | GET /api/items/:itemId | [200] name | string → -
RISKY (2)
risky | format-changed | GET /api/items/:itemId | [200] sku | string (uuid) → string
risky | enum-values-changed | GET /api/items/:itemId | [200] status | "active" | "archived" → "active" | "archived" | "draft"
INFO (3)
info | type-changed | GET /api/items/:itemId | [200] price | number (integer) → number
...
Semantics: side a is what consumers believe (an older recording, a committed
baseline, or a claims model extracted from your source), side b is observed
reality — breaking means code written against a breaks under b.
Gate it in CI:
# record against the new deploy, then:
wiretype gen --targets model --out baseline-check
wiretype diff baseline/model.json baseline-check/model.json --fail-on breaking
# Markdown report for PR comments, localized (en|ko):
wiretype diff baseline/model.json baseline-check/model.json --md --lang ko
--md prints a Markdown report (summary + one findings table per severity);
--lang localizes the headings and labels while machine fields (endpoints,
paths, before → after types) stay untranslated. --json is never localized.
The full rule set (nullability, optionality, enum widening, format loss, status changes, ...) is deterministic and documented in docs/ARCHITECTURE.md.
Claude agent plugin
The repo ships a Claude Code / Cowork plugin (claude-plugin/)
with an api-drift-audit skill: the agent finds your API call sites, extracts
what your hand-written types and zod schemas believe, converts that into a
claims model, and lets wiretype diff deliver the verdict — then maps every
breaking/risky finding to file:line and offers to fix types, refresh MSW mocks,
or add zod guards. The agent discovers and explains; the judgment stays
deterministic.
claude plugin marketplace add ehdrms785/wiretype
claude plugin install wiretype
# then: "run an API drift audit against the dev recording"
How is this different from…
- openapi-typescript / orval — those need a spec that's correct. wiretype needs no spec at all; it derives one (and catches where the real API disagrees with the docs).
- quicktype — quicktype types one JSON sample. wiretype merges many samples per endpoint (that's where optionals, nullables, and enums come from) and understands HTTP: routes, params, query, status codes.
- HAR-based generators — HAR exports are one-shot and manual. wiretype records continuously while you develop and regenerates in one command.
Limitations
- Inference is observation-based: fields never seen are never typed. Click more, get more.
- REST/JSON only — no GraphQL.
- WebSocket traffic passes through unrecorded.
- Bodies over 1 MiB are truncated and skipped for JSON parsing.