Mocking Tool Selection
This page walks through choosing among MSW, WireMock, Prism, Mockoon, json-server, and a local gateway for a specific project — and, just as importantly, what each of them is the wrong tool for.
Prerequisites
Before you can score any tool, inventory the facts that constrain the choice. Gather these first; every later decision references them.
- Runtimes that make requests — list each one: browser (
fetch/XHR), Node.js test process, native mobile simulator, backend service, CLI, gRPC client - Languages in play — is the test harness JavaScript/TypeScript only, or do JVM/Go/Python services also need the same mocks?
- Contract source — do you have an OpenAPI/JSON Schema spec, a Pact file, or only hand-written examples?
- Statefulness required — do any flows need a
POSTto be visible to a laterGET, or are all responses stateless? - CI constraints — what is your per-job time budget, and does the runner allow Docker (
services:containers) or only Node processes? - Protocol coverage — REST only, or also GraphQL, gRPC, or WebSocket?
- Team topology — one frontend team, or multiple polyglot services sharing one mock definition?
If you cannot answer the first bullet precisely, stop and collect it — every mis-selection in the troubleshooting section below traces back to a runtime that was overlooked during this step.
The one dimension you cannot configure around
Every mocking tool intercepts requests at a fixed point in the stack, and that point is not negotiable through configuration. A tool that lives inside the JavaScript runtime will never see a raw socket opened by a native app; a tool that lives at the network boundary will never introspect an in-process fetch before it is serialised. Understanding proxy vs inline mocking strategies is the foundation of this entire page: inline tools replace the call inside the runtime, while proxy tools intercept the genuine TCP connection.
Because interception layer is immovable, the evaluation begins there. The decision tree below routes you from “what makes the request?” to a shortlist of tools before any feature comparison happens.
The tree gives you a shortlist. The three-phase workflow below turns that shortlist into a committed choice.
Phase 1 — Inventory your interception needs
The prerequisites list captured raw facts; Phase 1 turns them into an interception map. For every runtime you listed, write down where its request can be caught. This is not yet about picking a product — it is about eliminating products that physically cannot see the traffic.
A React web app whose only outbound calls come from fetch inside the browser and from a Vitest process can be fully served by MSW handler configuration: the Service Worker catches the browser calls and setupServer catches the Node calls, sharing one handler array. The moment you add a React Native build talking to the same backend, that runtime opens a socket MSW cannot touch, and the map now demands a second interceptor at the network layer — a local API gateway or WireMock listening on a port the simulator points at.
Write the map as a table of runtime → interception point → candidate tools:
| Runtime | Where the request can be caught | Candidate tools |
|---|---|---|
Browser fetch/XHR |
Service Worker, or dev-server proxy | MSW worker, Vite/webpack proxy |
| Node.js test process | Runtime request interception | MSW setupServer |
| Native mobile simulator | Network port it is pointed at | Gateway, WireMock, Mockoon |
| Backend service (JVM/Go/etc.) | Network port / injected base URL | WireMock, Prism, gateway |
| CLI / shell script | Network host it resolves | Gateway, WireMock, json-server |
| gRPC client | HTTP/2 (h2c) endpoint |
Gateway (Traefik h2c), WireMock gRPC |
If a single row lists two runtimes with different interception points, you already know the project needs more than one tool — and the FAQ below covers running them together safely. The request interception patterns reference explains why the browser and Node interception points differ even for the same fetch call.
Phase 2 — Score tools against the decision matrix
With a shortlist per runtime, score the candidates across the dimensions that actually differentiate them. The matrix below is the core of this page. Read each column as a hard filter first (does the tool clear the bar at all?) and a tie-breaker second (which cleared candidate scores best?).
| Dimension | MSW | WireMock | Prism | Mockoon | json-server | Local gateway |
|---|---|---|---|---|---|---|
| Interception layer | Runtime (worker + node) | Network (HTTP server) | Network (HTTP server) | Network (HTTP server) | Network (HTTP server) | Network (proxy) |
| Primary language | JS/TS | JVM (lang-agnostic over HTTP) | Node (lang-agnostic over HTTP) | Standalone binary | Node | Config (Nginx/Traefik) |
| Statefulness | Manual, in-handler | Native scenarios | Stateless (spec examples) | Rule-based, in-memory | CRUD on JSON file | Delegates to upstream |
| OpenAPI-native | No (handlers are code) | Via extension/import | Yes — runs the spec directly | Import supported | No | No |
| Container-friendly | As a Node image | First-class image | First-class image | Docker image | Trivial Node image | First-class image |
| CI startup cost | Near-zero (in-process) | Seconds (JVM boot) | Low (Node boot) | Low (binary boot) | Near-zero | Low (proxy boot) |
| Best-fit sweet spot | Frontend + Node tests | Cross-language, stateful | Spec-conformance stubs | GUI-authored quick mocks | REST prototype from JSON | Multi-runtime routing |
| Wrong for | Native/backend sockets | Lightweight throwaway mocks | Conditional logic / state | Version-controlled team specs | Contract fidelity | Being the mock itself |
A few filters do most of the work:
- If any consumer is not JavaScript, MSW drops out for that consumer and you are choosing among the network-layer servers. For a spec you want honoured exactly, Prism runs your OpenAPI document as a live stub with no hand-written mappings. For scenario state across a sequence, WireMock’s scenarios are the reference implementation, which is why WireMock standalone configuration is the anchor for stateful cross-language work.
- If CI time is tight and the suite is JS-only, MSW’s in-process startup beats every container by seconds per job because nothing boots — the interceptor installs inside the test runner. The trade-off between MSW and a container server specifically for pipelines is deep enough to have its own comparison of MSW vs WireMock for CI pipelines.
- If you are prototyping a REST resource with no spec, json-server gives you full CRUD over a JSON file in one command — but it earns the “wrong for” note the instant you need contract fidelity or conditional responses.
- If a designer or non-coder authors the mocks, Mockoon’s GUI is the differentiator; the trade-off is that its
.jsonenvironment file is less pleasant to review in a pull request than code handlers or spec files.
When two candidates clear every hard filter, break the tie on the row that maps to your biggest future cost: statefulness if flows will grow, OpenAPI-native if the spec is the source of truth, CI startup if the pipeline is already slow.
Phase 3 — Pilot and integrate into the mock stack + CI
A matrix score is a hypothesis. Phase 3 tests it with the smallest possible working slice before you commit the team.
Step 1 — Stand up a minimal interceptor for the runtime that drove the choice. For a JS-only frontend that scored MSW, the pilot is a single handler proving the browser and Node paths share one definition:
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/v1/health', () =>
HttpResponse.json({ status: 'ok', source: 'msw-pilot' })
),
];
// src/mocks/server.ts — Node/Vitest interception
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
For a cross-language project that scored WireMock, the equivalent pilot is one mapping file the container serves to every runtime:
{
"request": { "method": "GET", "urlPath": "/api/v1/health" },
"response": {
"status": 200,
"jsonBody": { "status": "ok", "source": "wiremock-pilot" },
"headers": { "Content-Type": "application/json" }
}
}
Step 2 — Wire the interceptor into the mock stack. If MSW is the only tool, the stack is the test setup file. If the project also runs a network-layer server for other runtimes, place that server behind the same routing you already use for dockerized mock environments so one Compose file starts everything:
# docker-compose.mock.yml
services:
wiremock:
image: wiremock/wiremock:3.5.4
command: ["--port", "8080", "--verbose"]
volumes:
- "./wiremock/mappings:/home/wiremock/mappings:ro"
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/api/v1/health"]
interval: 5s
timeout: 3s
retries: 5
Step 3 — Prove it in CI. The pilot only counts if the pipeline exercises it. For MSW, that is the test job importing server; for a container, it is a health-gated service start:
# .github/workflows/mock-pilot.yml
name: Mock pilot
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
# JS-only path: MSW installs in-process, nothing to boot
- name: Run tests with MSW
run: npx vitest run
# Cross-language path: start the container and wait for health
- name: Start WireMock
run: docker compose -f docker-compose.mock.yml up -d --wait
- name: Confirm interception
run: curl -sf http://localhost:8080/api/v1/health | grep wiremock-pilot
If the pilot passes for every runtime on your Phase 1 map, promote it. If a runtime still reaches the real network, the tool was mis-scored — return to Phase 1 rather than layering a second tool as a patch.
Verification steps
The selection is validated when all of the following hold. Run them after the pilot.
- Every runtime on the Phase 1 map is intercepted:
# No unhandled requests escape the JS suite npx vitest run --reporter=verbose 2>&1 | grep -i "unhandled request" # Expected: no output - The network-layer server (if any) answers on its port:
curl -sf http://localhost:8080/api/v1/health | jq -r .source # Expected: wiremock-pilot (or your chosen tool's marker) - The mock stack starts inside your CI budget:
/usr/bin/time -v docker compose -f docker-compose.mock.yml up -d --wait 2>&1 | grep "Elapsed" # Expected: within your per-job time allowance - Adding a second endpoint needs only the chosen tool — no new dependency, no second server for the same runtime.
- A fresh clone reproduces the same interception with
npm ciand oneupcommand, with no per-developer manual steps.
Troubleshooting
Symptom: tests pass locally but a mobile build hits the real staging API
Diagnosis: MSW was selected for a project whose Phase 1 map included a native runtime. The Service Worker and setupServer only cover browser and Node; the simulator opens a socket MSW never sees. Fix: add a network-layer interceptor for the native runtime — point the simulator at a local API gateway or WireMock port. MSW stays for the web tests; the two coexist because each owns a different runtime.
Symptom: the mock server drifts from the API and nobody notices until integration
Diagnosis: a code-first tool (MSW handlers, hand-written WireMock mappings) was chosen where the OpenAPI spec is the real source of truth, so mappings and spec evolve independently. Fix: either switch the stubs to Prism so the spec is the mock, or add a validation gate that checks responses against the spec on every build. The trade-off is covered under proxy vs inline mocking strategies, where spec fidelity favours the network layer.
Symptom: CI jobs got 40 seconds slower after adding mocks
Diagnosis: a JVM-based WireMock container was chosen for a JS-only suite, so every shard now pays JVM boot time it does not need. Fix: for pure JavaScript tests, MSW installs in-process with near-zero startup. Reserve the container for the runtimes that genuinely cannot use it. The detailed pipeline comparison quantifies the difference.
Symptom: a POST succeeds but the follow-up GET returns the old value
Diagnosis: a stateless tool (Prism running spec examples, json-server without persistence semantics, or MSW without manual state) was chosen for a flow that needs sequence state. Fix: move the stateful endpoints to WireMock scenarios or Mockoon rules, or model the state explicitly in an MSW handler. Do not fight statelessness with per-test fixtures — it gets brittle as sequences grow.
Symptom: the mock file is impossible to review in pull requests
Diagnosis: Mockoon was chosen for a team that version-controls and reviews mocks, but its exported environment JSON is a large machine-authored blob. Fix: if review matters more than GUI authoring, prefer code handlers (MSW) or spec-driven stubs (Prism) whose diffs are meaningful. If the GUI is non-negotiable, split the Mockoon environment into smaller files by domain to keep diffs legible.
When to advance
You have finished tool selection and are ready to build out real handlers, mappings, and CI wiring when:
- The decision matrix has a single committed choice per runtime, with the “wrong for” note reviewed and accepted.
- The Phase 3 pilot intercepts every runtime on the map in both local and CI runs.
- Nobody on the team is quietly running a second, undocumented interceptor to cover a gap the selection missed.
- The mock stack starts, health-checks, and tears down on one command, ready to inherit the practices in mock lifecycle management.
At that point the choice is settled, and the two deep-dives below take over: the pipeline-specific comparison for CI, and the interception-layer comparison for proxy versus Service Worker mocks.
FAQ
Can one project use more than one mocking tool at once?
Yes, and mature stacks usually do. A React app commonly runs MSW for browser and Node tests while a local gateway or WireMock serves the same contract to a mobile simulator or backend service. The rule is that each runtime should have exactly one interceptor active at a time so responses are not double-handled. Keep a single source of truth — for example, generate WireMock mappings from the same fixtures your MSW handlers use — so the two layers never disagree.
Should I pick a tool by its feature list or by the runtime that makes the request?
Start from the runtime. The interception layer a tool operates at is the one dimension you cannot work around with configuration. MSW cannot see a native mobile socket no matter how you configure it, and a gateway cannot introspect an in-process fetch call. Feature lists — templating, scenarios, GUI authoring — only matter once the candidates that can physically see your traffic are on the table, which is exactly why Phase 1 precedes the matrix in Phase 2.
When is json-server the wrong choice?
json-server is excellent for a quick REST prototype backed by a JSON file, but it is the wrong choice when you need contract fidelity against an OpenAPI spec, per-request conditional logic, protocol coverage beyond REST, or scenario state that survives a defined sequence. Reach for Prism for spec fidelity, MSW or WireMock for conditional logic, and a stateful WireMock scenario for sequences. Treat json-server as a scaffold you outgrow, not a foundation you build on.
How do I know the selection was correct?
The choice is correct when every runtime that motivated it is intercepted without per-developer setup drift, the mock stack starts inside your CI time budget, and adding a new endpoint does not force a second tool. If any runtime still escapes to the real network, or CI startup balloons, revisit the inventory rather than patching around the symptom — nearly every mis-selection traces back to a runtime overlooked in Phase 1.
Related
- MSW vs WireMock for CI Pipelines — the pipeline-specific head-to-head once the shortlist is down to these two
- Choosing Between Proxy and Service Worker Mocks — deciding the interception layer for browser traffic specifically
- Proxy vs Inline Mocking Strategies — the architectural split that anchors the whole matrix
- Mock Service Worker (MSW) Setup — building out the JS-runtime choice into real handlers
- WireMock Standalone Configuration — building out the cross-language, stateful choice
← Back to API Mocking Fundamentals & Architecture