Skip to main content

API Mocking: MSW vs Mirage vs WireMock 2026

·APIScout Team
Share:

API Mocking for Development: MSW vs Mirage vs WireMock

Developing against live APIs is slow, costs money, and breaks when the API goes down. API mocking lets you develop, test, and demo without touching real APIs. But which mocking tool should you use? It depends on your stack, test runner, and what you're building.

The Landscape

ToolLanguageLayerBest For
MSW (Mock Service Worker)JS/TSNetwork (Service Worker / Node)Frontend + API testing
Mirage JSJS/TSApplication (in-memory)Frontend prototyping
WireMockJava (HTTP server)Network (HTTP proxy)Backend / language-agnostic
PrismCLINetwork (HTTP server)OpenAPI-driven mocking
json-serverJSNetwork (HTTP server)Quick REST API from JSON
NockNode.jsNetwork (Node http)Node.js unit tests
Polly.jsJS/TSNetwork (record/replay)Snapshot-based testing

MSW (Mock Service Worker)

What It Is

MSW intercepts HTTP requests at the network level using a Service Worker (browser) or request interceptor (Node.js). Your application code makes real fetch calls — MSW catches them before they leave.

Setup

npm install msw --save-dev
npx msw init public/ --save  # For browser usage
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  // GET endpoint
  http.get('https://api.example.com/users', () => {
    return HttpResponse.json([
      { id: '1', name: 'Alice', email: 'alice@example.com' },
      { id: '2', name: 'Bob', email: 'bob@example.com' },
    ]);
  }),

  // POST with request body
  http.post('https://api.example.com/users', async ({ request }) => {
    const body = await request.json() as { name: string; email: string };
    return HttpResponse.json(
      { id: crypto.randomUUID(), ...body },
      { status: 201 }
    );
  }),

  // Dynamic route params
  http.get('https://api.example.com/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Alice',
      email: 'alice@example.com',
    });
  }),

  // Error simulation
  http.delete('https://api.example.com/users/:id', () => {
    return HttpResponse.json(
      { error: 'Forbidden' },
      { status: 403 }
    );
  }),
];
// For tests (Node.js)
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

// vitest.setup.ts
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// For browser (development)
// mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);

// main.tsx
if (process.env.NODE_ENV === 'development') {
  const { worker } = await import('./mocks/browser');
  await worker.start();
}

When to Use MSW

  • ✅ Frontend development without backend
  • ✅ Integration tests (React Testing Library, Vitest, Jest)
  • ✅ E2E test data setup
  • ✅ Shared handlers between dev and test
  • ❌ Backend-only mocking (use WireMock or Nock)
  • ❌ Need persistent data across requests (use Mirage)

Mirage JS

What It Is

Mirage runs an in-memory server inside your JavaScript application. It includes a full ORM with relationships, factories, and serializers — making it ideal for prototyping with realistic data.

Setup

npm install miragejs --save-dev
import { createServer, Model, Factory, belongsTo, hasMany } from 'miragejs';

const server = createServer({
  models: {
    user: Model.extend({
      posts: hasMany(),
    }),
    post: Model.extend({
      author: belongsTo('user'),
    }),
  },

  factories: {
    user: Factory.extend({
      name(i: number) { return `User ${i}`; },
      email(i: number) { return `user${i}@example.com`; },
    }),
    post: Factory.extend({
      title(i: number) { return `Post ${i}`; },
      body() { return 'Lorem ipsum...'; },
    }),
  },

  seeds(server) {
    // Create 10 users with 3 posts each
    server.createList('user', 10).forEach(user => {
      server.createList('post', 3, { author: user });
    });
  },

  routes() {
    this.namespace = 'api';

    this.get('/users', (schema) => {
      return schema.users.all();
    });

    this.get('/users/:id', (schema, request) => {
      return schema.users.find(request.params.id);
    });

    this.post('/users', (schema, request) => {
      const attrs = JSON.parse(request.requestBody);
      return schema.users.create(attrs);
    });

    this.del('/users/:id', (schema, request) => {
      schema.users.find(request.params.id)?.destroy();
      return new Response(204);
    });
  },
});

When to Use Mirage

  • ✅ Prototyping with realistic, relational data
  • ✅ Frontend demos and presentations
  • ✅ Complex CRUD flows (create → list → update → delete)
  • ✅ Need stateful mock data (data persists during session)
  • ❌ Node.js / backend testing (browser-only)
  • ❌ Real network-level mocking (uses XMLHttpRequest patching)

WireMock

What It Is

WireMock is an HTTP server that acts as a mock API. It runs as a standalone process and intercepts real HTTP traffic — making it language-agnostic.

Setup

# Docker
docker run -d -p 8080:8080 wiremock/wiremock

# Or standalone JAR
java -jar wiremock-standalone-3.x.jar --port 8080
# Define mocks via API
curl -X POST http://localhost:8080/__admin/mappings -d '{
  "request": {
    "method": "GET",
    "urlPattern": "/api/users/.*"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": {
      "id": "123",
      "name": "Alice",
      "email": "alice@example.com"
    }
  }
}'

# Or via JSON files in mappings/ directory
# mappings/get-user.json
{
  "request": {
    "method": "GET",
    "urlPathPattern": "/api/users/[a-z0-9]+"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": {
      "id": "{{request.path.[2]}}",
      "name": "Alice"
    },
    "transformers": ["response-template"]
  }
}

When to Use WireMock

  • ✅ Backend integration testing (any language)
  • ✅ Contract testing
  • ✅ CI/CD pipelines (Docker-based)
  • ✅ Team-wide shared mock server
  • ❌ Quick frontend prototyping (too heavy)
  • ❌ Browser-based testing (use MSW)

Prism (OpenAPI-Driven)

What It Is

Prism generates a mock server directly from your OpenAPI specification — zero handler code.

# Install
npm install -g @stoplight/prism-cli

# Run mock server from OpenAPI spec
prism mock openapi.yaml
# Server starts at http://127.0.0.1:4010

# Request validation mode
prism proxy openapi.yaml https://api.example.com
# Validates requests/responses against spec

When to Use Prism

  • ✅ API-first development (design spec → mock → build)
  • ✅ Validating requests against OpenAPI spec
  • ✅ Quick mock from existing spec (zero code)
  • ❌ Custom mock logic (limited)
  • ❌ Stateful data (no persistence)

Comparison Matrix

FeatureMSWMirageWireMockPrism
Setup complexityLowLowMediumLow
Browser support
Node.js support✅ (HTTP)✅ (HTTP)
Network-level interception
Stateful data / ORM
OpenAPI integration
Record/replay
Language agnosticJS/TS onlyJS/TS only
TypeScript typesPartial
Bundle size~12KB~40KBN/A (server)N/A (CLI)

Choosing the Right Tool

ScenarioRecommended Tool
React/Vue/Svelte developmentMSW
Prototyping with complex dataMirage JS
Backend integration tests (any language)WireMock
API-first development with OpenAPIPrism
Quick REST mock from JSONjson-server
Node.js unit testsNock or MSW
Snapshot-based test replayPolly.js

Common Mistakes

MistakeImpactFix
Mocking too muchTests pass but integration breaksMock at network boundary, not function level
Not testing error casesOnly happy path worksAdd mock handlers for 4xx, 5xx, timeouts
Stale mock dataMock doesn't match real APIRefresh mocks when API changes
Mocking in productionUsers get fake dataOnly enable mocks in dev/test environments
Not matching real response shapeFrontend breaks with real APIUse OpenAPI spec to validate mock responses

MSW in Integration and E2E Testing

MSW is the only tool in this list that works across three different testing environments with the same handler code: Vitest/Jest (Node.js), Playwright (browser), and Storybook. This is its single most underrated feature.

Vitest integration: MSW for Node.js uses request interception middleware rather than Service Workers. The setup is three lines in your test setup file, and the handlers are identical to your browser handlers:

// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: { setupFiles: ['./src/mocks/setup.ts'] }
});

// src/mocks/setup.ts
import { server } from './server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

onUnhandledRequest: 'error' is valuable: if your component makes an API call not covered by a handler, the test fails rather than silently hanging. This catches accidental API calls you forgot to mock.

Overriding handlers per test: the default handlers cover the happy path. For specific tests, override them with server.use():

it('shows error state when API is down', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Service unavailable' }, { status: 503 });
    })
  );

  render(<UserList />);
  expect(await screen.findByText('Unable to load users')).toBeInTheDocument();
});

it('handles empty state', async () => {
  server.use(
    http.get('/api/users', () => HttpResponse.json([]))
  );

  render(<UserList />);
  expect(await screen.findByText('No users found')).toBeInTheDocument();
});

The handler override is scoped to the test — afterEach resets to the default handlers automatically.

Playwright integration: MSW works in Playwright tests through the browser's Service Worker — the same worker.js that runs in development. Install the worker in your Playwright setup, and your existing browser handlers intercept requests during E2E tests:

// playwright.setup.ts
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();

// MSW Service Worker intercepts all fetch calls
await page.addInitScript(() => {
  // Worker registers in the page context
});
await page.goto('http://localhost:3000');

This gives you Playwright tests that don't depend on a running backend — every API call is intercepted by MSW, making E2E tests fast and deterministic.

Testing Error Scenarios and Edge Cases

A mocking setup that only covers the happy path gives false confidence. APIs fail in specific, observable ways that your frontend must handle: 401s (session expired), 429s (rate limited), 503s (service down), and network timeouts. Each scenario requires different UI behavior.

// handlers.ts — include error scenarios as named handlers
export const errorHandlers = {
  unauthorized: http.get('/api/users', () =>
    HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
  ),
  rateLimited: http.get('/api/users', () =>
    HttpResponse.json({ error: 'Rate limit exceeded' }, {
      status: 429,
      headers: { 'Retry-After': '60' }
    })
  ),
  networkError: http.get('/api/users', () =>
    HttpResponse.error()  // Simulates a network-level failure (fetch throws)
  ),
  timeout: http.get('/api/users', async () => {
    await new Promise(resolve => setTimeout(resolve, 30000));  // Will be aborted
    return HttpResponse.json([]);
  }),
};

Use these handlers in dedicated test suites for error boundary components, loading states, and retry logic. Test that your 401 handler redirects to login, your 429 handler shows a "try again later" message with the retry countdown, and your network error handler shows the offline indicator. These tests catch the regressions that production incidents reveal.

Contract Testing with WireMock

WireMock supports contract testing via its response stubbing and verification API — you define the expected request/response contract, and your tests assert that the contract was honored.

The WireMock Java client provides a fluent API for contract verification. For JavaScript/TypeScript projects, WireMock's REST API is the interface:

// wiremock-contracts.ts — verify API contracts in CI
const WIREMOCK_URL = process.env.WIREMOCK_URL ?? 'http://localhost:8080';

async function verifyGetUser() {
  // Reset request log
  await fetch(`${WIREMOCK_URL}/__admin/requests/reset`, { method: 'DELETE' });

  // Make the actual API call (against WireMock)
  const response = await fetch(`${WIREMOCK_URL}/api/users/123`);
  const data = await response.json();

  // Verify WireMock received the expected request
  const verifyResponse = await fetch(`${WIREMOCK_URL}/__admin/requests/find`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      method: 'GET',
      urlPath: '/api/users/123',
    }),
  });

  const { requests } = await verifyResponse.json();
  console.assert(requests.length === 1, 'Expected exactly one GET /api/users/123 request');
  console.assert(data.id === '123', 'Expected user id in response');
}

WireMock's contract testing shines in backend-to-backend integration: Service A mocks Service B's API with WireMock stubs. The stubs are the contract. When Service B's API changes, the stubs break in CI — catching breaking changes before deployment rather than after.

WireMock in CI/CD Pipelines

WireMock's Docker image makes it straightforward to run in any CI environment. A common pattern: start WireMock as a service container in your GitHub Actions workflow, load your mock definitions, run integration tests against it, then tear it down.

# .github/workflows/integration-tests.yml
services:
  wiremock:
    image: wiremock/wiremock:3.x
    ports:
      - 8080:8080
    volumes:
      - ./mocks/wiremock:/home/wiremock/mappings

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      EXTERNAL_API_URL: http://localhost:8080
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration

Your integration tests set EXTERNAL_API_URL to the WireMock instance instead of the real API. No API keys needed, no rate limits, no flaky external dependencies. This pattern works for any backend language — the WireMock server is language-agnostic, and your application just changes an environment variable.

WireMock's stateful scenarios feature lets you model APIs that return different responses on successive calls — simulating a payment that succeeds on the third attempt, a status endpoint that transitions from pending to processing to complete, or a webhook delivery endpoint that returns 503 twice before succeeding. This is much harder to simulate with static mocks.

Record and Replay with Polly.js

Polly.js (by Netflix, open source) takes a different approach: instead of hand-writing mocks, it records real HTTP traffic and replays it in tests. First run: requests go through to the real API, responses are recorded to disk. Subsequent runs: responses are replayed from the recording without hitting the real API.

import { Polly } from '@pollyjs/core';
import NodeFetchAdapter from '@pollyjs/adapter-node-fetch';
import FSPersister from '@pollyjs/persister-fs';

Polly.register(NodeFetchAdapter);
Polly.register(FSPersister);

const polly = new Polly('my-api-recording', {
  adapters: ['node-fetch'],
  persister: 'fs',
  persisterOptions: {
    fs: { recordingsDir: '__recordings__' },
  },
  recordIfMissing: true,  // Record on first run, replay thereafter
  matchRequestsBy: {
    method: true,
    url: true,
    body: false,  // Ignore request body for matching
  },
});

// Your test code runs normally — Polly intercepts fetch
const response = await fetch('https://api.example.com/data');
const data = await response.json();

await polly.stop();

Polly.js is most useful when the API is complex (many endpoints, complex responses), hand-writing mocks would be tedious, and the recorded data is representative of real-world usage. The limitation: recorded responses go stale when the real API changes, requiring periodic re-recording against the live API.

Methodology

Tool versions referenced: MSW 2.x (major rewrite from v1; API changed significantly), Mirage JS 0.1.x, WireMock 3.x (Spring-free standalone), Prism 5.x by Stoplight, json-server 1.x, Nock 14.x, Polly.js 6.x. MSW bundle size (~12KB) is for the Node.js adapter; the browser Service Worker bundle is larger. WireMock Java JAR size not applicable to frontend bundle comparisons. All tool documentation versions verified as of March 2026. The "network-level interception" distinction refers to whether the tool intercepts at the HTTP transport layer (true for MSW, WireMock, Prism) or patches JavaScript's fetch/XMLHttpRequest at the application layer (Mirage, Nock).

Choosing based on team context: the most important selection criterion after the browser/backend split is your team's existing test tooling. MSW has first-class integrations with React Testing Library, Vitest, Jest, Playwright, Cypress, and Storybook — if your team uses any of these, MSW likely slots in with minimal configuration. WireMock requires running an external process (Java or Docker) that some frontend teams find operationally heavier than a Node.js-only tool. Mirage's ORM is uniquely powerful for teams prototyping data-heavy UIs — it's the only tool that gives you hasMany and belongsTo relationships in your mock data model, which matters when your real API returns nested resources and your tests need to set up realistic relational data without a database. Prism's killer feature is zero-maintenance mocks: add an endpoint to your OpenAPI spec and Prism automatically serves a valid response without writing a handler. For API-first teams where the OpenAPI spec is the source of truth, this eliminates a whole category of stale mock maintenance. Prism also runs in proxy mode — forwarding requests to the real API while validating that both the request and response conform to the spec. This makes it a contract testing tool as well as a mock server, useful for catching API drift between what the spec documents and what the real server actually returns.


Find APIs with the best testing and sandbox support on APIScout — sandbox environments, mock data generators, and developer experience ratings.

Related: How AI Is Transforming API Design and Documentation, OpenAI Realtime API: Building Voice Applications 2026, Anthropic Claude API: Developer Guide 2026

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.