Today Platform Web — Dev Docs
ToolchainTesting

MSW handlers and fixtures

MSW (Mock Service Worker) is the mock-mode runtime — handlers + fixtures live under `apps/web/src/lib/msw/` and are also the source of truth for Storybook stories that need network responses.

MSW (Mock Service Worker, catalog msw: ^2.13.4) is how apps/web runs without a real backend. It backs:

  • Storybook stories that need API responses
  • Unit tests that exercise components hitting fetch
  • mockMode=true in the running app — see apps/web/src/lib/msw/msw-gate.tsx

The Playwright e2e suites (smoke / full) do not use MSW — they hit a real Vercel preview URL.

Layout

apps/web/src/lib/msw/
├── msw-gate.tsx              Provider that conditionally enables MSW at runtime
├── mock-worker.ts            Worker setup + lifecycle (`start`, `stop`)
├── handlers/
│   ├── index.ts              Re-exports all handlers
│   ├── agent.ts              POST /v1/messages, GET /events SSE, ...
│   ├── auth.ts               better-auth endpoints
│   ├── automations.ts
│   ├── channels.ts
│   ├── connectors.ts
│   ├── devices.ts
│   ├── diaries.ts
│   ├── messages.ts
│   ├── onboarding.ts
│   ├── skills.ts
│   ├── tasks.ts
│   ├── timeline.ts
│   ├── today-pages.ts
│   ├── today-pages-v2.ts
│   ├── today-pages-v2-tckb.ts
│   └── user.ts
├── fixtures/
│   ├── index.ts              Re-exports
│   ├── agent.ts              Sample message threads, SSE event streams
│   ├── auth.ts               Session shapes
│   ├── automations.ts
│   ├── ...                   (one fixture file per resource, mirroring handlers)
│   └── tckb/
│       └── README.md         TCK widget fixture conventions
├── handlers.test.ts          Unit tests against the handler set
├── fixtures-contract.test.ts Smoke-tests fixtures stay shaped like the real API
└── mock-worker.test.ts

The split is mirror-image: every resource has one handlers/<resource>.ts and one fixtures/<resource>.ts. Handlers describe the request → response flow; fixtures describe the data shape that flows through.

Why the split

A fixture is reusable across multiple handlers and across non-MSW code paths (Storybook decorators, unit tests that don't need a worker). Keeping fixtures data-only and handlers stateful/transport-only means:

  • Storybook can import { userFixture } from '@/lib/msw/fixtures/user' and pass it directly to a component prop without booting MSW
  • Unit tests can stub a single handler with a custom fixture variant (http.get('/api/user', () => HttpResponse.json({ ...userFixture, name: 'X' }))) without the whole MSW worker being involved
  • The contract test (fixtures-contract.test.ts) validates fixtures still match the generated OpenAPI types after the codegen step regenerates them

How handlers are activated

apps/web decides whether to enable MSW at runtime via msw-gate.tsx:

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

isMockModeEnabled() checks a ?mockMode=true query param plus a feature flag, so individual pages and Storybook stories can flip it without affecting the rest of the app.

In Storybook, the same gate is mounted in apps/web/.storybook/preview.ts so any story renders with MSW enabled. Story-specific handler overrides go through Storybook's parameters.msw.handlers convention.

Adding a handler

// apps/web/src/lib/msw/handlers/widgets.ts
import { http, HttpResponse } from 'msw'
import { widgetsFixture } from '../fixtures/widgets'

export const widgetsHandlers = [
  http.get('/api/widgets', () => HttpResponse.json(widgetsFixture.list)),
  http.post('/api/widgets', async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json({ ...widgetsFixture.created, ...body }, { status: 201 })
  }),
]
// apps/web/src/lib/msw/fixtures/widgets.ts
export const widgetsFixture = {
  list: [
    /* ... */
  ],
  created: {
    /* ... */
  },
}

Then re-export from handlers/index.ts and fixtures/index.ts. The contract test will pick the new fixture up automatically; if its shape diverges from the generated OpenAPI types, the test fails and points at the field.

What MSW does not mock

  • SSE streams that emit > tens of events. MSW supports SSE but the worker thread has practical limits; the agent SSE handler in handlers/agent.ts uses a bounded fixture stream
  • Token streaming beyond a small canned set (tier-2 improvement in TODO.md)
  • WebSocket transports — none in the app currently

For everything else, MSW is the right layer. Avoid vi.mock('node-fetch') / vi.mock('axios') style mocks; they are brittle and miss the request shape that MSW exercises.

Worker lifecycle

mock-worker.ts exports setupMockWorker() which is what MswGate calls. It registers all handlers, starts the worker, and returns a cleanup hook. Tests that want per-test handler overrides use the worker.use(...) / worker.resetHandlers() pattern — see apps/web/src/lib/msw/handlers.test.ts for the canonical setup.

On this page