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:

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

JobBest fitWhy
Unit/component tests for one component's statesMSWColocated handlers, per-test overrides, no server process
Storybook stories with edge-case dataMSWSame reason; state lives with the story
Whole-app development before backend existsSpec-driven (workspace or Prism)Real HTTP semantics, contract fidelity, one source
Request validation while developingPrism / spec-drivenMalformed calls fail loudly against the contract
Create-then-fetch journeysSpec-drivenStateful scenarios; MSW needs hand-written state
SSE/WebSocket before the channel existsSpec-drivenEmits frames from documented event schemas
CI contract gatePrism strict modeDeterministic validation of requests against spec
End-to-end tests against stagingNone of the aboveHit 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:
// 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 runs a sample spec in the browser, and this post covers unblocking three teams with mocks.

What to read next: the best OpenAPI mock servers in 2026 compares Prism, Microcks, Postman, and spec-driven mocks on a real spec, and scenario testing for REST APIs defines the journey format.