Today Platform Web — Dev Docs
Architecture

MSW mocks

MSW (Mock Service Worker) is the architectural mock layer — Storybook stories, unit tests, and the runtime `mockMode=true` path all share the same handlers and fixtures.

MSW is not just a testing tool in this repo — it's a first-class architectural layer that mock-runs the entire app without a backend. Three consumers share the same handlers + fixtures:

ConsumerWhy
Storybook storiesComponents that hit the network need responses to render
Unit tests in apps/web/src/Component behaviors-under-test that exercise fetch
The running app with mockMode=trueA live demo path: full apps/web UX, zero cloud dependencies

The implementation lives under apps/web/src/lib/msw/ — see Toolchain → Testing → MSW for the file layout. This page covers why MSW is structured this way, not how to add a handler.

Mock mode is a runtime decision

The web app decides whether to enable MSW at runtime via MswGate:

function MswGate({ children }) {
  if (!isMockModeEnabled()) return children
  return <MswProvider handlers={allHandlers}>{children}</MswProvider>
}

isMockModeEnabled() checks a ?mockMode=true query parameter plus a feature flag. So:

  • Production users: query param not set → MSW never installs → real network
  • A demo session: ?mockMode=true → MSW intercepts every request → app runs entirely on fixtures
  • A specific page in Storybook: per-story parameters.msw.handlers override

The gate's separation between "user-runtime mock mode" and "build-time test mode" lets product demos run on the same code path as unit tests. They share the same fixtures and the same handlers.

Handlers + fixtures, side by side

The directory split:

apps/web/src/lib/msw/
├── handlers/<resource>.ts     transport — request → response
└── fixtures/<resource>.ts     data — the JSON shape

The handlers describe what to do when a request comes in. The fixtures describe what the data looks like in a shape-only sense. A test that needs a custom variant overrides just the fixture; a test that needs custom status codes overrides the handler. Both can live without booting the full MSW worker — import { userFixture } from '@/lib/msw/fixtures/user' is data-only and free of MSW dependencies.

The contract test that keeps fixtures honest

apps/web/src/lib/msw/fixtures-contract.test.ts runs the generated OpenAPI Zod schemas (from pnpm api:generate) against every fixture in the codebase. When a backend schema changes:

  1. Cloud team updates the OpenAPI spec
  2. Someone runs pnpm api:generate on web
  3. New Zod schemas land in src/lib/api/generated/zod.gen.ts
  4. fixtures-contract.test.ts fails on every fixture that no longer matches
  5. Fix the fixtures (or — rarely — push back on the schema change)

This catches the most common drift between mock data and real responses: fixtures going stale silently. The test failure tells you exactly which field shape diverged.

Why MSW and not vi.mock('axios') style mocks

Tooling alternatives we considered and rejected:

ApproachWhy it's worse
vi.mock('fetch') per testEach test repaints the mock setup; no shared baseline
vi.mock('@hey-api/client-fetch')Tied to the codegen client; brittle when codegen changes
Custom mockFetch() injectionRequires every fetch caller to accept a dependency injection seam
MSWIntercepts at the network layer; same setup for tests and runtime mock

MSW intercepts at the fetch/XHR boundary, so:

  • The app's code paths run unchanged in mock mode — same useApiClient, same agent-client, same headers, same retry logic
  • Storybook stories don't have to know they're in mock mode (the gate above the story's tree handles it)
  • Unit tests that exercise a specific failure mode can override one handler via worker.use(http.get('/api/X', () => HttpResponse.error())) without re-mocking the whole stack

What MSW does not do

  • No SSE-stream replay with token-level fidelity. The agent handler in handlers/agent.ts emits a small canned event stream. Token-level streaming with realistic timing is a tier-2 improvement (tracked in TODO.md).
  • No WebSocket support. The app doesn't currently use WebSockets; if it ever does, MSW will need configuration to handle them.
  • No production-shaped error responses for every endpoint. The handlers emit success responses by default; tests opt into error responses per-test via worker.use(...).

When to add a new handler

You're hitting a new backend endpoint from apps/web for the first time:

  1. Run pnpm api:generate if the cloud team has added the endpoint to the OpenAPI spec
  2. Add a fixture in lib/msw/fixtures/<resource>.ts matching the generated Zod schema
  3. Add a handler in lib/msw/handlers/<resource>.ts returning the fixture
  4. Re-export both from the respective index.ts
  5. fixtures-contract.test.ts will pick up the new fixture automatically

Skipping these steps means Storybook breaks (no response → component shows loading forever) and unit tests have to import handlers individually. The mirror pattern keeps it cheap.

On this page