# Mocking APIs for frontend development: MSW vs Prism vs spec-driven mocks

Frontend teams have three mocking jobs that blur together until a release goes wrong: developing against an API that does not exist yet, component and integration tests that must not hit the network, and full-journey verification against a running service. Each job has a tool that fits it, and most pain comes from using one tool for all three. This compares MSW (Mock Service Worker), Stoplight Prism, and spec-driven mocks from an OpenAPI workspace, then shows the combination that avoids fixture sprawl.

## What each tool actually is

**MSW** intercepts requests at the network layer using a Service Worker in the browser and a request interceptor in Node. Handlers are code:

``` javascript
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("https://api.local/v1/projects/:id", ({ params }) =>
    HttpResponse.json({
      data: { id: params.id, name: "Apollo", status: "ready" },
    })
  ),
];
```

Because handlers live in the frontend codebase, they are trivially available in Vitest/Jest and Storybook, they can encode UI state (empty list, one item, permission denied, slow network), and developers override them per test. The cost: responses are hand-written JavaScript. Nothing validates them against the real API contract, and when the backend renames `data.name` to `data.title`, the mock keeps returning `name` and the tests stay green until production.

**Prism** is an HTTP server that mocks from an OpenAPI document. In dynamic mode it synthesizes responses from schemas; in static mode it serves documented examples. It also validates incoming requests against the spec and rejects malformed calls with 422s. Its strength is contract fidelity: the mock cannot drift from the document because the document is the mock. Its limits: stateless responses (create-then-fetch does not return what you created), no SSE streams beyond static bodies, and running it is an operational step rather than an in-browser default.

**Spec-driven workspace mocks** (Powerduck) also serve from the OpenAPI document, but add two things the other tools lack: scenario journeys that carry state across requests (create returns an id, the following GET returns that resource), and real SSE/WebSocket behavior driven by documented event contracts. The mock runs locally with no account, and the same spec feeds the debugger and the test runner. The tradeoff versus MSW: it is an HTTP service, not browser-native test infrastructure, so it is not the natural choice for unit-level component tests.

## Where each belongs

| Job                                             | Best fit                         | Why                                                       |
|-------------------------------------------------|----------------------------------|-----------------------------------------------------------|
| Unit/component tests for one component's states | MSW                              | Colocated handlers, per-test overrides, no server process |
| Storybook stories with edge-case data           | MSW                              | Same reason; state lives with the story                   |
| Whole-app development before backend exists     | Spec-driven (workspace or Prism) | Real HTTP semantics, contract fidelity, one source        |
| Request validation while developing             | Prism / spec-driven              | Malformed calls fail loudly against the contract          |
| Create-then-fetch journeys                      | Spec-driven                      | Stateful scenarios; MSW needs hand-written state          |
| SSE/WebSocket before the channel exists         | Spec-driven                      | Emits frames from documented event schemas                |
| CI contract gate                                | Prism strict mode                | Deterministic validation of requests against spec         |
| End-to-end tests against staging                | None of the above                | Hit the real service                                      |

## The fixture sprawl anti-pattern

The failure mode is three independent sources of truth: backend has the OpenAPI document, QA has a Postman collection with saved responses, frontend has MSW handlers with inline JSON. Every field rename is a three-way merge nobody coordinates, and the mocks become fiction in three different genres.

The maintainable arrangement points everything at the contract:

1.  **The OpenAPI document is the source**, with realistic examples on every schema. Examples are the highest-leverage content in the document because the mock, the docs, and the SDK all consume them.
2.  **MSW handlers stay, but they do not invent shapes.** Generate or derive their base responses from the spec's examples — either by generating handler data from the OpenAPI document in a build step, or by having MSW proxy to a local Prism/spec server for the default case and override only the state a specific test needs:

``` javascript
// Default: proxy to the spec-driven mock running locally.
// Per-test: override specific routes with edge-case states.
useCase?.handlers ?? defaultProxyHandlers;
```

1.  **Edge cases stay in MSW because that is their home.** "Show the retry banner when the server returns 429 three times" is UI choreography; encode it in the test layer, not in the contract.
2.  **Journeys live in scenarios**, not in handlers. The create-then-fetch-then-cancel sequence is a spec scenario runnable against the mock and staging; duplicating it in MSW is what makes the suite lie.

## The network boundary question

Teams sometimes ask why they need a real HTTP mock at all when MSW exists. The answer is what MSW cannot exercise: actual serialization (a body the server rejects as invalid JSON never fails), content-type negotiation, compression, cookies, redirects, streaming responses, and — critically — whether the frontend's request *matches the contract*. A Service Worker that returns your own JSON will accept any request shape. Prism's strict mode and spec-driven validation catch the frontend calling `POST /project` (singular) with `planCode` instead of `plan_code` before the backend exists to 404 it.

Conversely, do not try to unit test a button's loading spinner against an HTTP server process; that is what MSW does best. The tools are layers, not competitors.

## A working split for a React codebase

-   **Vitest + Testing Library + MSW**: component and hook tests; handlers colocated, edge cases explicit.
-   **Local development**: spec-driven mock server running from the OpenAPI document, including journeys and streams; frontend points its API base URL at it.
-   **CI contract job**: Prism in strict mode against the merged spec, replaying a recorded set of real frontend requests to catch contract violations.
-   **CI e2e**: Playwright against the spec-driven mock for unbuilt features, against staging for built ones, using the same scenario definitions.
-   **Spec maintenance**: examples edited in the OpenAPI document, never in fixture files; generated fixture data flows from examples to MSW defaults.

The result is one contract and three consumers, with hand-written mock code limited to the UI states that genuinely need it.

Powerduck is the spec-driven layer in that stack — local mocks including SSE, scenario journeys shared between mock and staging, and a debugger that sends real requests against the same document. The [demo](https://www.powerduck.com/app/?ref=powerduck.com) runs a sample spec in the browser, and [this post covers unblocking three teams with mocks](https://www.powerduck.com/blog/dont-block-frontend-work-on-a-missing-backend/).

**What to read next:** [the best OpenAPI mock servers in 2026](https://www.powerduck.com/blog/best-openapi-mock-servers-2026/) compares Prism, Microcks, Postman, and spec-driven mocks on a real spec, and [scenario testing for REST APIs](https://www.powerduck.com/blog/openapi-scenario-testing-user-journeys/) defines the journey format.

