npm.io
0.2.0 • Published 19h agoCLI

wiretype

Licence
MIT
Version
0.2.0
Deps
1
Size
385 kB
Vulns
0
Weekly
0

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.

CI npm license

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/7GET /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

  1. 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.
  2. 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.
  3. 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.

License

MIT daro

Keywords