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 POST to be visible to a later GET, 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.

Mocking Tool Selection Decision Tree Starting from the question "what makes the request?", browser traffic routes to MSW or a dev-server proxy, Node.js test traffic routes to MSW node, native mobile or backend or CLI traffic routes to a local gateway or WireMock, and a cross-language contract routes to WireMock or Prism. What makes the request? Browser fetch / XHR Node.js test Vitest / Jest Native / backend mobile · service · CLI Cross-language shared contract MSW (worker) or dev-server proxy MSW (node) setupServer Gateway / WireMock network layer WireMock / Prism spec-driven stub Then refine by second question need stateful sequences or an OpenAPI spec? Stateful sequence WireMock scenarios · Mockoon Spec-first, stateless Prism · json-server (REST)

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 .json environment 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 ci and one up command, 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.


← Back to API Mocking Fundamentals & Architecture