API Mocking: MSW vs Mirage vs WireMock 2026
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
| Tool | Language | Layer | Best For |
|---|---|---|---|
| MSW (Mock Service Worker) | JS/TS | Network (Service Worker / Node) | Frontend + API testing |
| Mirage JS | JS/TS | Application (in-memory) | Frontend prototyping |
| WireMock | Java (HTTP server) | Network (HTTP proxy) | Backend / language-agnostic |
| Prism | CLI | Network (HTTP server) | OpenAPI-driven mocking |
| json-server | JS | Network (HTTP server) | Quick REST API from JSON |
| Nock | Node.js | Network (Node http) | Node.js unit tests |
| Polly.js | JS/TS | Network (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
| Feature | MSW | Mirage | WireMock | Prism |
|---|---|---|---|---|
| Setup complexity | Low | Low | Medium | Low |
| Browser support | ✅ | ✅ | ❌ | ❌ |
| Node.js support | ✅ | ❌ | ✅ (HTTP) | ✅ (HTTP) |
| Network-level interception | ✅ | ❌ | ✅ | ✅ |
| Stateful data / ORM | ❌ | ✅ | ❌ | ❌ |
| OpenAPI integration | ❌ | ❌ | ✅ | ✅ |
| Record/replay | ❌ | ❌ | ✅ | ❌ |
| Language agnostic | JS/TS only | JS/TS only | ✅ | ✅ |
| TypeScript types | ✅ | Partial | ❌ | ❌ |
| Bundle size | ~12KB | ~40KB | N/A (server) | N/A (CLI) |
Choosing the Right Tool
| Scenario | Recommended Tool |
|---|---|
| React/Vue/Svelte development | MSW |
| Prototyping with complex data | Mirage JS |
| Backend integration tests (any language) | WireMock |
| API-first development with OpenAPI | Prism |
| Quick REST mock from JSON | json-server |
| Node.js unit tests | Nock or MSW |
| Snapshot-based test replay | Polly.js |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Mocking too much | Tests pass but integration breaks | Mock at network boundary, not function level |
| Not testing error cases | Only happy path works | Add mock handlers for 4xx, 5xx, timeouts |
| Stale mock data | Mock doesn't match real API | Refresh mocks when API changes |
| Mocking in production | Users get fake data | Only enable mocks in dev/test environments |
| Not matching real response shape | Frontend breaks with real API | Use 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