API Mocking for Development: MSW vs Mirage vs WireMock
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 |
Find APIs with the best testing and sandbox support on APIScout — sandbox environments, mock data generators, and developer experience ratings.