wiremock-ts
v0.8.0
Published
A TypeScript-first HTTP mock server — WireMock, reimagined.
Maintainers
Readme
wiremock-ts
A TypeScript-first HTTP mock server — WireMock, reimagined for the Node/TS ecosystem.
Stand up a real HTTP server that returns the responses you stub, match incoming requests on almost anything, and verify what your code actually sent — through a fully-typed fluent API, a REST admin API, or a CLI. The same stubs can also serve in-process by intercepting fetch, with no socket at all.
It runs on Node's built-in http/https. Runtime dependencies are zod (schemas), jsonpath-plus (JSONPath matching), ajv (OpenAPI request validation) and ws (WebSocket mocking).
Contents
- Install
- Quick start
- Two ways to serve
- HTTPS
- Request matching
- Responses
- Programmatic responses
- Response templating
- Stateful scenarios
- Fault injection
- Proxying
- Record and playback
- Contract-first (OpenAPI)
- GraphQL
- WebSockets
- Verification
- Remote client SDK
- CLI
- Admin API
- API reference
- Scripts
- Roadmap
- Security
- Contributing
- Licence
Install
npm install --save-dev wiremock-tsRequires Node 20+. Ships as ESM with type declarations.
Quick start
import { equalToJson, get, ok, okJson, post, urlPathEqualTo, WireMockServer } from "wiremock-ts";
const wm = await new WireMockServer({ port: 8080 }).start();
wm.stubFor(get(urlPathEqualTo("/api/user")).willReturn(okJson({ id: 1, name: "ada" })));
wm.stubFor(
post(urlPathEqualTo("/orders"))
.withRequestBody(equalToJson({ sku: "abc" }))
.willReturn(ok("accepted")),
);
// point your code at wm.baseUrl, then verify what it sent:
wm.verify({ method: "POST", urlPath: "/orders" }, 1); // true if called exactly once
await wm.stop();Use port: 0 to bind a free ephemeral port (ideal for parallel tests) and read the chosen address from wm.baseUrl. In tests, await using cleans up automatically:
import { get, ok, startMock, urlPathEqualTo } from "wiremock-ts";
it("works", async () => {
await using wm = await startMock({ port: 0 });
wm.stubFor(get(urlPathEqualTo("/ping")).willReturn(ok("pong")));
// ... the server is stopped when the block exits
});Two ways to serve
The same stubs can be served two ways:
- A real HTTP server (
start()) — anything that speaks HTTP can hit it. - In-process
fetchinterception (interceptFetch()) — no socket;fetchcalls are answered from the registry. Unmatched requests pass through to the realfetchby default.
const wm = new WireMockServer(); // not started
wm.stubFor(get(urlPathEqualTo("/api/ping")).willReturn(okJson({ pong: true })));
wm.interceptFetch();
await fetch("https://example.test/api/ping"); // served in-process, no network
wm.restoreFetch();Pass interceptFetch({ passthrough: false }) to return 404 for unmatched requests instead of hitting the network.
HTTPS
Pass a PEM key and certificate to serve over TLS; everything else (stubbing, the admin API, verification) is unchanged, and baseUrl reports an https:// URL.
const wm = await new WireMockServer({
https: { key: pemKey, cert: pemCert },
}).start();
// wm.baseUrl === "https://127.0.0.1:<port>"Bring your own certificate (for a fixed hostname) or generate a throwaway self-signed pair for tests.
Request matching
A request matches when every constraint declared on the pattern is satisfied. An empty pattern matches anything.
| Field | Matches on |
| ----------------- | --------------------------------------------------- |
| method | HTTP method, or "ANY" |
| url | exact path and query string |
| urlPath | exact path only |
| urlPathPattern | regex against the path |
| urlPattern | regex against path + query |
| queryParameters | per-parameter content pattern |
| headers | per-header content pattern (case-insensitive names) |
| bodyPatterns | content patterns against the raw body |
URL matchers and content matchers are exposed as builder helpers:
import {
absent,
containing,
equalTo,
equalToJson,
get,
matching,
matchingJsonPath,
notMatching,
urlEqualTo,
urlMatching,
urlPathEqualTo,
urlPathMatching,
} from "wiremock-ts";
wm.stubFor(
get(urlPathMatching("^/users/\\d+$"))
.withQueryParam("expand", equalTo("profile"))
.withHeader("authorization", matching("^Bearer "))
.willReturn(ok()),
);Content patterns: equalTo(value, caseInsensitive?), containing, matching / notMatching (regex), equalToJson(value, { ignoreExtraElements?, ignoreArrayOrder? }), matchingJsonPath, and absent.
When several stubs match, the lowest priority wins (set with .atPriority(n)); on a tie the most recently registered stub wins.
Responses
Build responses fluently:
import { aResponse, created, noContent, notFound, ok, okJson, status } from "wiremock-ts";
aResponse()
.withStatus(202)
.withHeader("x-trace", "abc")
.withJsonBody({ queued: true })
.withFixedDelay(150); // msok(body?), okJson(json), created(body?), noContent(), notFound(body?) and status(code) are shortcuts. A response can carry body, jsonBody, or base64Body, plus headers, a fixed delay, a template flag, a fault, or a proxy target.
Programmatic responses
Pass a function to willReturn to compute the response from the request — fully typed, no string templating required. This is in-process only (used with start() or interceptFetch()).
wm.stubFor(
get(urlPathMatching("^/echo")).willReturn((req) =>
okJson({ method: req.method, path: req.urlPath, query: req.query }),
),
);Response templating
For declarative (serialisable) stubs, opt in with .withTransform() and use {{ ... }} tokens in the body and header values:
aResponse()
.withBody("you requested {{request.path}} at {{now}} (id {{randomUuid}})")
.withTransform();Supported tokens: request.method, request.url, request.path, request.body, request.query.<key>, request.headers.<key>, now, and randomUuid. Unknown tokens render as empty.
Stateful scenarios
Model a sequence of responses with a per-scenario state machine. A stub only matches while its scenario is in requiredScenarioState (the implicit start state is "Started"), and serving it transitions to newScenarioState.
wm.stubFor(
get(urlPathEqualTo("/job"))
.inScenario("job")
.whenScenarioStateIs("Started")
.willSetStateTo("done")
.willReturn(okJson({ status: "pending" })),
);
wm.stubFor(
get(urlPathEqualTo("/job"))
.inScenario("job")
.whenScenarioStateIs("done")
.willReturn(okJson({ status: "complete" })),
);Fault injection
Return a connection-level failure instead of a normal response:
wm.stubFor(get(urlPathEqualTo("/flaky")).willReturn(aResponse().withFault("connection-reset")));Faults: connection-reset, empty-response, malformed-chunk, random-then-close. Under interceptFetch() a fault surfaces as a rejected fetch (network error).
Proxying
Forward a matched request to a real upstream and return its response:
import { proxiedFrom } from "wiremock-ts";
wm.stubFor(get(urlMatching("^/v1/")).willReturn(proxiedFrom("https://api.example.com")));Record and playback
Capture live traffic against a real backend, then replay it offline:
wm.startRecording("https://api.example.com");
// ... drive your app; every request is proxied and captured ...
const mappings = wm.stopRecording();
// replay later, with the backend unavailable:
for (const mapping of mappings) wm.stubFor(mapping);Contract-first (OpenAPI)
Generate a working mock from an OpenAPI 3 document — one stub per operation, with response bodies generated from the schemas ($ref resolution, example/default/enum, and format-aware strings such as uuid, email, date-time).
import openapi from "./openapi.json" with { type: "json" };
wm.loadOpenApi(openapi); // registers a stub for every path + methodUse stubsFromOpenApi(doc) to get the mappings without registering them, or generateSample(schema, doc) to build representative data from a single schema.
Pass { validateRequests: true } to hold callers to the contract: operations with a request body schema validate incoming bodies with ajv and answer 400 (with the violations) when they do not conform, instead of returning the success response.
wm.loadOpenApi(openapi, { validateRequests: true });
// a request whose body omits a required field or uses the wrong type:
// → 400 { "error": "Request body failed schema validation", "errors": [ ... ] }$refs into #/components/schemas resolve against the document. compileRequestBodyValidators(doc) and validateRequestBody(validator, body) are exported if you want to validate without a server.
GraphQL
Stub a GraphQL endpoint by operation name. GraphQL posts { query, operationName, variables }; graphQlRequest matches on operationName (ignoring the rest of the body), with an optional substring check against the query.
import { graphQlRequest, graphQlRequestedFor, okJson } from "wiremock-ts";
wm.stubFor(graphQlRequest("GetUser").willReturn(okJson({ data: { user: { id: 1 } } })));
// finer control, and a custom path:
wm.stubFor(
graphQlRequest("Search", { path: "/api/graphql", queryContains: "first: 10" }).willReturn(
okJson({ data: { search: [] } }),
),
);
// verify a GraphQL operation was called:
wm.verify(graphQlRequestedFor("GetUser"));The endpoint defaults to /graphql; override it with the path option. It returns the same builder as post(...), so all the usual response, scenario and priority methods apply.
WebSockets
Register a WebSocket endpoint at an exact path. Connecting clients receive scripted messages on connect, an echo of what they send, or a computed reply.
// push messages as soon as the client connects:
wm.stubWebSocket("/feed", { onConnect: ["hello", "world"] });
// echo everything back:
wm.stubWebSocket("/echo", { echo: true });
// compute a reply per message (return undefined to stay silent):
wm.stubWebSocket("/rpc", { reply: (msg) => (msg === "ping" ? "pong" : undefined) });Connections to a path with no stub are refused. WebSocket endpoints are served on the same port (over TLS too when https is set, via wss://).
Verification
Assert what your code sent, either by boolean or by throwing:
import { containing, postRequestedFor, urlPathEqualTo } from "wiremock-ts";
wm.verify(postRequestedFor(urlPathEqualTo("/orders")), 1); // boolean
wm.assertReceived(postRequestedFor(urlPathEqualTo("/orders"))); // throws if never received
wm.countRequests({ method: "GET", urlPath: "/health" }); // number
wm.findRequests(postRequestedFor(urlPathEqualTo("/orders")).withRequestBody(containing("abc")));getRequestedFor, postRequestedFor, putRequestedFor, deleteRequestedFor, patchRequestedFor and anyRequestedFor build verification patterns and accept .withHeader / .withQueryParam / .withRequestBody.
Remote client SDK
Drive a server running elsewhere (another process, a container, a CI service) over its admin API, using the same DSL builders. WireMockClient mirrors the stubbing and verification surface; its methods are async.
import { connectMock, get, getRequestedFor, okJson, urlPathEqualTo } from "wiremock-ts";
const client = connectMock("http://localhost:8080", {
adminToken: process.env.WIREMOCK_ADMIN_TOKEN,
});
await client.stubFor(get(urlPathEqualTo("/ping")).willReturn(okJson({ pong: true })));
// ... drive your system-under-test ...
await client.verify(getRequestedFor(urlPathEqualTo("/ping")), 1);
await client.resetAll();Methods: register / stubFor, listMappings, countRequests, findRequests, verify, assertReceived, resetMappings, resetRequests, resetAll. Programmatic (function) responses run in-process only, so registering one through the client throws.
CLI
wiremock-ts --port 8080 --host 127.0.0.1 --mappings ./mappings--mappings <dir> loads every *.json file in the directory; each file is either a single stub mapping or { "mappings": [ ... ] }.
Admin API
Served on the same port under /__admin:
| Method & path | Purpose |
| ------------------------------- | --------------------------------- |
| GET /__admin/health | Liveness |
| GET /__admin/mappings | List stubs |
| POST /__admin/mappings | Register a stub (201) |
| GET /__admin/mappings/{id} | Fetch one stub |
| DELETE /__admin/mappings/{id} | Delete one stub |
| POST /__admin/mappings/reset | Clear all stubs |
| GET /__admin/requests | Request journal |
| DELETE /__admin/requests | Clear the journal |
| POST /__admin/requests/count | Count requests matching a pattern |
| POST /__admin/requests/find | Find requests matching a pattern |
| POST /__admin/reset | Reset stubs and journal |
Request bodies are validated with zod; malformed JSON or schema-invalid mappings return 400.
API reference
WireMockServer (and startMock(options), which constructs and starts in one call):
- Lifecycle:
start(),stop(),baseUrl,port,[Symbol.asyncDispose](options:port,host,adminToken,allowedProxyHosts,https) - Stubbing:
stubFor(mapping)/register(mapping),listMappings(),loadOpenApi(doc, { validateRequests? }),stubWebSocket(path, options) - Verification:
verify(pattern, count?),assertReceived(pattern, count?),countRequests(pattern),findRequests(pattern) - Reset:
resetMappings(),resetRequests(),resetAll() - Record:
startRecording(targetBaseUrl),stopRecording(),isRecording - Interception:
interceptFetch(options?),restoreFetch()
pattern accepts a plain RequestPattern or any *RequestedFor(...) builder. Builders, content matchers, response builders, the GraphQL helpers, the OpenAPI helpers, and WireMockClient / connectMock are all exported from the package root.
Scripts
| Script | Description |
| ----------------------- | ------------------------------------------ |
| npm run dev | Run the CLI under tsx watch |
| npm run build | Bundle to dist/ (ESM + .d.ts) via tsup |
| npm test | Run the Vitest suite |
| npm run test:coverage | Run the suite with a coverage report |
| npm run typecheck | tsc --noEmit |
| npm run lint | ESLint (flat config) |
| npm run format | Prettier (formats and sorts imports) |
Roadmap
- gRPC mocking (HTTP/2 + protobuf)
- Response-body schema validation against the contract
Security
wiremock-ts is a development and testing tool. It binds to 127.0.0.1 by default — keep it there unless you have a reason not to, because the admin API and the proxy are powerful and unauthenticated out of the box.
The admin API is unauthenticated by default. Anyone who can reach the port can register or delete stubs, reset state, and read the request journal — which holds the headers and bodies your system-under-test sent, including any credentials. Set
adminTokento requireAuthorization: Bearer <token>(orX-Admin-Token: <token>) on every/__adminrequest:const wm = await new WireMockServer({ adminToken: process.env.WIREMOCK_ADMIN_TOKEN }).start();Proxy and record can reach arbitrary URLs (SSRF).
proxiedFrom,proxyBaseUrlandstartRecordingmake the server issue outbound requests. Restrict them withallowedProxyHosts— only listed hostnames may be proxied; anything else is refused with502(or a thrown error when recording):const wm = await new WireMockServer({ allowedProxyHosts: ["api.example.com"] }).start();interceptFetch()patches globalfetch— it's for tests, andstop()/restoreFetch()restore the original.
To report a vulnerability, see SECURITY.md.
Contributing
Contributions are welcome — please read CONTRIBUTING.md first. It covers the project's conventions (exact-pinned dependencies, extensionless ESM imports, named exports, the ESLint/Prettier setup) and the typecheck → lint → format:check → test → build gate every change must pass.
Licence
MIT © Matt Hesketh
