API Mocking Tools: MSW, WireMock, Microcks, and Prism
API Mocking Tools: MSW, WireMock, Microcks, and Prism
API mocking is one of those practices that feels optional until you need it -- and then you realize you needed it months ago. Frontend teams waiting on backend endpoints, integration tests hitting flaky external services, mobile apps developing against APIs that do not exist yet: all of these problems go away with a good mocking strategy.
The challenge is that "API mocking" means different things depending on where you intercept traffic and how you define mock behavior. This guide covers the four major approaches and helps you pick the right tool for your architecture.
Mocking Approaches: Where Do You Intercept?
Before comparing tools, it helps to understand the three fundamental approaches to API mocking:
| Approach | How It Works | Best For | Tools |
|---|---|---|---|
| In-process interception | Hooks into your HTTP client at the network level within your application process | Unit/integration tests, browser dev | MSW |
| Standalone mock server | Runs a separate HTTP server that your app calls | Integration testing, polyglot stacks | WireMock, Microcks |
| Spec-driven mocking | Generates mock responses from an API specification | Contract testing, API-first design | Prism, Microcks |
Each approach has different trade-offs in setup complexity, realism, and maintenance burden.
MSW (Mock Service Worker): In-Process Interception
MSW intercepts outgoing HTTP requests at the network level using a Service Worker (in the browser) or by patching Node.js request modules (in tests). Your application code makes real fetch() or axios calls -- MSW catches them before they leave the process and returns mock responses.
This is the key insight: your application code does not change. No special mock clients, no environment flags to swap base URLs. The same code that calls your real API in production calls MSW in tests.
Setup for Node.js Tests
npm install msw --save-dev
// src/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
// GET with query parameters
http.get("/api/users", ({ request }) => {
const url = new URL(request.url);
const page = url.searchParams.get("page") || "1";
return HttpResponse.json({
users: [
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" },
],
page: parseInt(page),
totalPages: 5,
});
}),
// POST with request body validation
http.post("/api/users", async ({ request }) => {
const body = await request.json();
if (!body.name || !body.email) {
return HttpResponse.json(
{ error: "Name and email are required" },
{ status: 400 }
);
}
return HttpResponse.json(
{ id: 3, ...body, createdAt: new Date().toISOString() },
{ status: 201 }
);
}),
// Error scenarios
http.get("/api/users/:id", ({ params }) => {
if (params.id === "999") {
return HttpResponse.json({ error: "User not found" }, { status: 404 });
}
if (params.id === "error") {
return HttpResponse.error(); // Network error
}
return HttpResponse.json({ id: params.id, name: "Alice" });
}),
// Delayed response (simulate slow APIs)
http.get("/api/reports", async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return HttpResponse.json({ status: "complete", data: [] });
}),
];
// src/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// test setup (vitest example)
import { beforeAll, afterAll, afterEach } from "vitest";
import { server } from "./mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
The onUnhandledRequest: "error" option is important. It throws an error when your code makes an HTTP request that has no matching handler. This catches missing mocks early instead of letting requests silently fail or hit real services.
Per-Test Handler Overrides
import { http, HttpResponse } from "msw";
import { server } from "./mocks/server";
test("handles server error gracefully", async () => {
// Override the default handler just for this test
server.use(
http.get("/api/users", () => {
return HttpResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
})
);
const result = await fetchUsers();
expect(result.error).toBe("Failed to load users");
});
Browser Integration (for Development)
// src/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
// src/main.ts (start mocking in development only)
async function enableMocking() {
if (process.env.NODE_ENV !== "development") return;
const { worker } = await import("./mocks/browser");
return worker.start({
onUnhandledRequest: "bypass", // let unmocked requests through
});
}
enableMocking().then(() => {
// boot your app
});
Strengths: Zero application code changes, works in both browser and Node.js, TypeScript-native, excellent for testing error/edge cases, handlers are colocated with your code and version-controlled.
Weaknesses: JavaScript/TypeScript only. Cannot mock at the protocol level (no gRPC, WebSockets require separate handling). In-process interception means it cannot test actual network behavior (DNS resolution, TLS, connection pooling).
WireMock: The Polyglot Mock Server
WireMock runs as a standalone HTTP server. Your application talks to it as if it were the real API. This makes it language-agnostic -- any HTTP client in any language can use WireMock mocks.
Running WireMock
# Docker (simplest)
docker run -p 8080:8080 wiremock/wiremock:latest
# With persistent mappings
docker run -p 8080:8080 \
-v ./wiremock/mappings:/home/wiremock/mappings \
-v ./wiremock/__files:/home/wiremock/__files \
wiremock/wiremock:latest
# Standalone JAR
java -jar wiremock-standalone-3.x.x.jar --port 8080
Defining Stubs
WireMock stubs are JSON files in the mappings/ directory:
{
"request": {
"method": "GET",
"urlPathPattern": "/api/users/[0-9]+",
"headers": {
"Authorization": { "matches": "Bearer .+" }
}
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"id": 1,
"name": "Alice",
"email": "[email protected]"
}
}
}
Or configure stubs via the admin API at runtime:
curl -X POST http://localhost:8080/__admin/mappings \
-H "Content-Type: application/json" \
-d '{
"request": {
"method": "POST",
"url": "/api/orders",
"bodyPatterns": [
{ "matchesJsonPath": "$.items[?(@.quantity > 0)]" }
]
},
"response": {
"status": 201,
"jsonBody": { "orderId": "ORD-12345", "status": "created" },
"fixedDelayMilliseconds": 500
}
}'
Advanced Features
WireMock supports stateful scenarios -- mocks that change behavior based on previous requests:
{
"scenarioName": "Order Flow",
"requiredScenarioState": "Started",
"newScenarioState": "Order Created",
"request": { "method": "POST", "url": "/api/orders" },
"response": { "status": 201, "jsonBody": { "orderId": "ORD-001" } }
}
It also supports record-and-playback -- point WireMock at a real API, and it records all responses as stubs that you can replay later:
# Start in record mode
java -jar wiremock-standalone.jar --record-mappings --proxy-all="https://api.example.com"
Strengths: Language-agnostic, advanced matching (XPath, JSONPath, regex), stateful scenarios, record-and-playback, battle-tested in enterprise environments, Docker-friendly.
Weaknesses: JVM dependency (unless using Docker), more complex setup than in-process mocking, stub files can get verbose, the admin API has a learning curve.
Microcks: Contract-Driven Mock Server
Microcks takes a different angle. Instead of writing mock definitions by hand, you feed it your API specifications (OpenAPI, AsyncAPI, gRPC protobuf, GraphQL schemas, Postman collections) and it generates a fully functional mock server automatically.
Running Microcks
# Docker Compose (recommended)
docker compose -f docker-compose.yml up -d
# Or use the Microcks-provided compose file
curl -sL https://microcks.io/quickstart | sh
Importing an API Specification
# Import an OpenAPI spec
curl -X POST http://localhost:8080/api/artifact/upload \
-F "[email protected]"
Once imported, Microcks automatically creates mock endpoints for every operation in your spec. If your OpenAPI spec defines GET /users with an example response, Microcks serves that example response at http://localhost:8080/rest/MyAPI/1.0/users.
Using with OpenAPI Examples
The quality of Microcks mocks depends entirely on the examples in your spec:
# openapi.yaml
paths:
/users:
get:
operationId: listUsers
responses:
'200':
description: List of users
content:
application/json:
examples:
twoUsers:
value:
- id: 1
name: Alice
- id: 2
name: Bob
emptyList:
value: []
Microcks uses the named examples to serve different responses. You select which example to return via a x-microcks-operation header or query parameter.
Strengths: Specification-driven (mocks stay in sync with your API contract), supports OpenAPI, AsyncAPI, gRPC, GraphQL, and Postman collections, excellent for contract testing, built-in test runner for validating that real implementations match the spec.
Weaknesses: Heavier setup (requires MongoDB and Keycloak in production mode), mock quality is only as good as your spec examples, less flexible for ad-hoc or scenario-based mocking compared to WireMock or MSW.
Prism: OpenAPI Mock Server
Prism (from Stoplight) is a lightweight mock server that generates responses from an OpenAPI specification. It is simpler than Microcks and focused purely on OpenAPI.
Basic Usage
# Install
npm install -g @stoplight/prism-cli
# Start mock server from an OpenAPI spec
prism mock openapi.yaml
# With dynamic response generation (random data matching the schema)
prism mock openapi.yaml --dynamic
# Validate requests against the spec (returns 422 for invalid requests)
prism proxy openapi.yaml https://real-api.example.com
The --dynamic flag is worth highlighting. Instead of returning static example values, Prism generates random data that matches your schema constraints. A string field with format: email returns a random email address. An integer with minimum: 1 and maximum: 100 returns a random number in that range. This is useful for catching bugs that only appear with varied data.
Prism's proxy mode is equally valuable. It sits between your client and the real API, validating every request and response against the spec. This catches contract violations in real-time.
Strengths: Single command to run, validates requests against your spec, dynamic response generation, proxy mode for contract validation, lightweight (Node.js, no JVM or database).
Weaknesses: OpenAPI only (no gRPC, AsyncAPI, or GraphQL), no stateful scenarios, no record-and-playback, limited to what your spec defines.
Comparison
| Feature | MSW | WireMock | Microcks | Prism |
|---|---|---|---|---|
| Approach | In-process | Standalone server | Standalone server | Standalone server |
| Language support | JS/TS only | Any (HTTP) | Any (HTTP/gRPC) | Any (HTTP) |
| Spec-driven | No | Plugin | Yes (primary) | Yes (primary) |
| Dynamic responses | Manual | Templating | From examples | Schema-based |
| Stateful scenarios | Manual | Built-in | Limited | No |
| Record & playback | No | Yes | No | No |
| Request validation | No | Pattern matching | Spec-based | Spec-based |
| Setup complexity | Low | Medium | High | Low |
| Best for | JS/TS testing | Integration testing | Contract testing | API-first development |
Choosing the Right Tool
You are building a JavaScript/TypeScript application and need mocks for tests and local development. Use MSW. It requires the least setup, produces the cleanest test code, and handles both browser and Node.js environments. Your mock handlers live alongside your application code and are version-controlled.
You have a polyglot microservices architecture and need shared mocks across teams. Use WireMock. It is language-agnostic, supports complex matching and stateful scenarios, and runs anywhere Docker runs. Record-and-playback mode is invaluable for creating mocks from existing services.
You practice API-first design and want mocks auto-generated from specs. Use Prism for lightweight OpenAPI mocking, or Microcks if you also need AsyncAPI, gRPC, or built-in contract testing. The choice between them comes down to how much infrastructure you want to run.
You need mocks in CI for integration tests. WireMock in Docker is the safest bet. It starts up quickly, stubs are declarative JSON files, and it works with any test framework in any language.
In practice, many teams use more than one. MSW for unit and component tests (fast, in-process), WireMock or Prism for integration tests (realistic network behavior), and Microcks for contract testing across teams. These tools are complementary, not competing.