Back to skills
SkillHub ClubShip Full StackFull StackBackendDesigner

msw

MSW (Mock Service Worker) v2 best practices, patterns, and API guidance for API mocking in JavaScript/TypeScript tests and development. Covers handler design, server setup, response construction, testing patterns, GraphQL, and v1-to-v2 migration. Baseline: msw ^2.0.0. Triggers on: msw imports, http.get, http.post, HttpResponse, setupServer, setupWorker, graphql.query, mentions of "msw", "mock service worker", "api mocking", or "msw v2".

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,131
Hot score
99
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-msw-skill

Repository

openclaw/skills

Skill path: skills/anivar/msw-skill

MSW (Mock Service Worker) v2 best practices, patterns, and API guidance for API mocking in JavaScript/TypeScript tests and development. Covers handler design, server setup, response construction, testing patterns, GraphQL, and v1-to-v2 migration. Baseline: msw ^2.0.0. Triggers on: msw imports, http.get, http.post, HttpResponse, setupServer, setupWorker, graphql.query, mentions of "msw", "mock service worker", "api mocking", or "msw v2".

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend, Designer, Testing.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install msw into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding msw to shared team environments
  • Use msw for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: msw
description: >
  MSW (Mock Service Worker) v2 best practices, patterns, and API guidance for
  API mocking in JavaScript/TypeScript tests and development. Covers handler
  design, server setup, response construction, testing patterns, GraphQL, and
  v1-to-v2 migration. Baseline: msw ^2.0.0.
  Triggers on: msw imports, http.get, http.post, HttpResponse, setupServer,
  setupWorker, graphql.query, mentions of "msw", "mock service worker",
  "api mocking", or "msw v2".
license: MIT
user-invocable: false
agentic: false
compatibility: "TypeScript/JavaScript projects using msw ^2.0.0"
metadata:
  author: Anivar Aravind
  author_url: https://anivar.net
  source_url: https://github.com/anivar/msw-skill
  version: 1.0.0
  tags: msw, mocking, api, testing, http, graphql, service-worker, fetch
---

# MSW (Mock Service Worker)

**IMPORTANT:** Your training data about `msw` may be outdated or incorrect — MSW v2 completely removed the `rest` namespace, `res(ctx.*)` response composition, and `(req, res, ctx)` resolver signature. Always rely on this skill's rule files and the project's actual source code as the source of truth. Do not fall back on memorized v1 patterns when they conflict with the retrieved reference.

## When to Use MSW

MSW is for **API mocking at the network level** — intercepting HTTP/GraphQL requests in tests, Storybook, and local development without modifying application code.

| Need | Recommended Tool |
|------|-----------------|
| Test API integration (React, Vue, Node) | **MSW** |
| Storybook API mocking | **MSW** (browser worker) |
| Local development without backend | **MSW** (browser worker) |
| Unit testing pure functions | Plain test doubles |
| E2E testing real APIs | Playwright/Cypress network interception |
| Mocking module internals | `vi.mock()` / `jest.mock()` |

## Quick Reference — v2 Essentials

```typescript
// Imports
import { http, HttpResponse, graphql, delay, bypass, passthrough } from 'msw'
import { setupServer } from 'msw/node'     // tests, SSR
import { setupWorker } from 'msw/browser'  // Storybook, dev

// Handler
http.get('/api/user/:id', async ({ request, params, cookies }) => {
  return HttpResponse.json({ id: params.id, name: 'John' })
})

// Server lifecycle (tests)
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

// Per-test override
server.use(
  http.get('/api/user/:id', () => new HttpResponse(null, { status: 500 }))
)

// Concurrent test isolation
it.concurrent('name', server.boundary(async () => {
  server.use(/* scoped overrides */)
}))
```

## Rule Categories by Priority

| Priority | Category | Impact | Prefix | Rules |
|----------|----------|--------|--------|-------|
| 1 | Handler Design | CRITICAL | `handler-` | 4 |
| 2 | Setup & Lifecycle | CRITICAL | `setup-` | 3 |
| 3 | Request Reading | HIGH | `request-` | 2 |
| 4 | Response Construction | HIGH | `response-` | 3 |
| 5 | Test Patterns | HIGH | `test-` | 4 |
| 6 | GraphQL | MEDIUM | `graphql-` | 2 |
| 7 | Utilities | MEDIUM | `util-` | 2 |

## All 20 Rules

### Handler Design (CRITICAL)

| Rule | File | Summary |
|------|------|---------|
| Use `http` namespace | `handler-use-http-namespace.md` | `rest` is removed in v2 — use `http.get()`, `http.post()` |
| No query params in URL | `handler-no-query-params.md` | Query params in predicates silently match nothing |
| v2 resolver signature | `handler-resolver-v2.md` | Use `({ request, params, cookies })`, not `(req, res, ctx)` |
| v2 response construction | `handler-response-v2.md` | Use `HttpResponse.json()`, not `res(ctx.json())` |

### Setup & Lifecycle (CRITICAL)

| Rule | File | Summary |
|------|------|---------|
| Correct import paths | `setup-import-paths.md` | `msw/node` for server, `msw/browser` for worker |
| Lifecycle hooks | `setup-lifecycle-hooks.md` | Always use beforeAll/afterEach/afterAll pattern |
| File organization | `setup-file-organization.md` | Organize in `src/mocks/` with handlers, node, browser files |

### Request Reading (HIGH)

| Rule | File | Summary |
|------|------|---------|
| Clone in events | `request-clone-events.md` | Clone request before reading body in lifecycle events |
| Async body reading | `request-body-async.md` | Always `await request.json()` — body reading is async |

### Response Construction (HIGH)

| Rule | File | Summary |
|------|------|---------|
| HttpResponse for cookies | `response-use-httpresponse.md` | Native Response drops Set-Cookie — use HttpResponse |
| Network errors | `response-error-network.md` | Use `HttpResponse.error()`, don't throw in resolvers |
| Streaming | `response-streaming.md` | Use ReadableStream for SSE/chunked responses |

### Test Patterns (HIGH)

| Rule | File | Summary |
|------|------|---------|
| Test behavior | `test-behavior-not-requests.md` | Assert on UI/state, not fetch call arguments |
| Per-test overrides | `test-override-with-use.md` | Use `server.use()` for error/edge case tests |
| Concurrent isolation | `test-concurrent-boundary.md` | Wrap concurrent tests in `server.boundary()` |
| Unhandled requests | `test-unhandled-request.md` | Set `onUnhandledRequest: 'error'` |

### GraphQL (MEDIUM)

| Rule | File | Summary |
|------|------|---------|
| Response shape | `graphql-response-shape.md` | Return `{ data }` / `{ errors }` via HttpResponse.json |
| Endpoint scoping | `graphql-scope-with-link.md` | Use `graphql.link(url)` for multiple GraphQL APIs |

### Utilities (MEDIUM)

| Rule | File | Summary |
|------|------|---------|
| bypass vs passthrough | `util-bypass-vs-passthrough.md` | `bypass()` = new request; `passthrough()` = let through |
| delay behavior | `util-delay-behavior.md` | `delay()` is instant in Node.js — use explicit ms |

## Response Method Quick Reference

| Method | Use for |
|--------|---------|
| `HttpResponse.json(data, init?)` | JSON responses |
| `HttpResponse.text(str, init?)` | Plain text |
| `HttpResponse.html(str, init?)` | HTML content |
| `HttpResponse.xml(str, init?)` | XML content |
| `HttpResponse.formData(fd, init?)` | Form data |
| `HttpResponse.arrayBuffer(buf, init?)` | Binary data |
| `HttpResponse.error()` | Network errors |

## v1 to v2 Migration Quick Reference

| v1 | v2 |
|----|-----|
| `import { rest } from 'msw'` | `import { http, HttpResponse } from 'msw'` |
| `rest.get(url, resolver)` | `http.get(url, resolver)` |
| `(req, res, ctx) => res(ctx.json(data))` | `() => HttpResponse.json(data)` |
| `req.params` | `params` from resolver info |
| `req.body` | `await request.json()` |
| `req.cookies` | `cookies` from resolver info |
| `res.once(...)` | `http.get(url, resolver, { once: true })` |
| `res.networkError()` | `HttpResponse.error()` |
| `ctx.delay(ms)` | `await delay(ms)` |
| `ctx.data({ user })` | `HttpResponse.json({ data: { user } })` |

## References

| Reference | Covers |
|-----------|--------|
| `handler-api.md` | `http.*` and `graphql.*` methods, URL predicates, path params |
| `response-api.md` | `HttpResponse` class, all static methods, cookie handling |
| `server-api.md` | `setupServer`/`setupWorker`, lifecycle events, `boundary()` |
| `test-patterns.md` | Vitest/Jest setup, overrides, concurrent isolation, cache clearing |
| `migration-v1-to-v2.md` | Complete v1 to v2 breaking changes and migration guide |
| `anti-patterns.md` | 10 common mistakes with BAD/GOOD examples |


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# MSW Skill

Created by **[Anivar Aravind](https://anivar.net)**

An AI agent skill for writing, reviewing, and debugging MSW (Mock Service Worker) v2 handlers, server setup, and test patterns with modern best practices.

## The Problem

AI agents often generate outdated MSW v1 patterns — `rest.get()` instead of `http.get()`, `res(ctx.json(...))` instead of `HttpResponse.json()`, `(req, res, ctx)` instead of `({ request, params })` — and miss critical testing best practices like `server.boundary()` for concurrent tests, `onUnhandledRequest: 'error'`, and proper lifecycle hook setup. These produce code that fails to compile or silently misbehaves at runtime.

## This Solution

20 rules with incorrect/correct code examples that teach agents MSW v2's actual API, handler design, server lifecycle, response construction, testing patterns, GraphQL usage, and v1-to-v2 migration. Each rule targets a specific mistake and shows exactly how to fix it.

## Install

```bash
npx skills add anivar/msw-skill -g
```

Or with full URL:

```bash
npx skills add https://github.com/anivar/msw-skill
```

## Baseline

- msw ^2.0.0
- TypeScript/JavaScript

## What's Inside

### 20 Rules Across 7 Categories

| Priority | Category | Rules | Impact |
|----------|----------|-------|--------|
| 1 | Handler Design | 4 | CRITICAL |
| 2 | Setup & Lifecycle | 3 | CRITICAL |
| 3 | Request Reading | 2 | HIGH |
| 4 | Response Construction | 3 | HIGH |
| 5 | Test Patterns | 4 | HIGH |
| 6 | GraphQL | 2 | MEDIUM |
| 7 | Utilities | 2 | MEDIUM |

Each rule file contains:
- Why it matters
- Incorrect code with explanation
- Correct code with explanation
- Decision tables and additional context

### 6 Deep-Dive References

| Reference | Covers |
|-----------|--------|
| `handler-api.md` | `http.*` and `graphql.*` methods, URL predicates, path params, handler options |
| `response-api.md` | `HttpResponse` class, all 7 static methods, cookie handling |
| `server-api.md` | `setupServer`/`setupWorker`, lifecycle events, `boundary()`, `onUnhandledRequest` |
| `test-patterns.md` | Vitest/Jest setup, per-test overrides, concurrent isolation, cache clearing |
| `migration-v1-to-v2.md` | Complete v1 to v2 breaking changes with migration mapping |
| `anti-patterns.md` | 10 common mistakes with BAD/GOOD examples |

## Structure

```
├── SKILL.md                          # Entry point for AI agents
├── AGENTS.md                         # Compiled guide with all rules expanded
├── rules/                            # Individual rules (Incorrect/Correct)
│   ├── handler-*                     # Handler design (CRITICAL)
│   ├── setup-*                       # Setup & lifecycle (CRITICAL)
│   ├── request-*                     # Request reading (HIGH)
│   ├── response-*                    # Response construction (HIGH)
│   ├── test-*                        # Test patterns (HIGH)
│   ├── graphql-*                     # GraphQL (MEDIUM)
│   └── util-*                        # Utilities (MEDIUM)
└── references/                       # Deep-dive reference docs
    ├── handler-api.md
    ├── response-api.md
    ├── server-api.md
    ├── test-patterns.md
    ├── migration-v1-to-v2.md
    └── anti-patterns.md
```

## Ecosystem — Skills by [Anivar Aravind](https://anivar.net)

### Testing Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [jest-skill](https://github.com/anivar/jest-skill) | Jest best practices — mock design, async testing, matchers, timers, snapshots | `npx skills add anivar/jest-skill -g` |
| [zod-testing](https://github.com/anivar/zod-testing) | Zod schema testing — safeParse, mock data, property-based | `npx skills add anivar/zod-testing -g` |
| [redux-saga-testing](https://github.com/anivar/redux-saga-testing) | Redux-Saga testing — expectSaga, testSaga, providers | `npx skills add anivar/redux-saga-testing -g` |

### Library & Framework Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [zod-skill](https://github.com/anivar/zod-skill) | Zod v4 schema validation, parsing, error handling | `npx skills add anivar/zod-skill -g` |
| [redux-saga-skill](https://github.com/anivar/redux-saga-skill) | Redux-Saga effects, fork model, channels, RTK | `npx skills add anivar/redux-saga-skill -g` |

### Engineering Analysis
| Skill | What it covers | Install |
|-------|---------------|---------|
| [contributor-codebase-analyzer](https://github.com/anivar/contributor-codebase-analyzer) | Code analysis, annual reviews, promotion readiness | `npx skills add anivar/contributor-codebase-analyzer -g` |

## Author

**[Anivar Aravind](https://anivar.net)** — Building AI agent skills for modern JavaScript/TypeScript development.

## License

MIT

```

### _meta.json

```json
{
  "owner": "anivar",
  "slug": "msw-skill",
  "displayName": "MSW (Mock Service Worker)",
  "latest": {
    "version": "1.0.1",
    "publishedAt": 1772684727962,
    "commit": "https://github.com/openclaw/skills/commit/6750cfb0f5396e6d811fd2028fa0e0f88fcee738"
  },
  "history": []
}

```

### references/anti-patterns.md

```markdown
# MSW Anti-Patterns

## Table of Contents

1. [Using v1 `rest` namespace](#1-using-v1-rest-namespace)
2. [Putting query params in URL predicates](#2-putting-query-params-in-url-predicates)
3. [Importing setupServer from wrong path](#3-importing-setupserver-from-wrong-path)
4. [Missing afterEach(resetHandlers)](#4-missing-aftereachresethandlers)
5. [Asserting on fetch calls](#5-asserting-on-fetch-calls)
6. [Not awaiting request.json()](#6-not-awaiting-requestjson)
7. [Using native Response for cookies](#7-using-native-response-for-cookies)
8. [Not using server.boundary() for concurrent tests](#8-not-using-serverboundary-for-concurrent-tests)
9. [Not setting onUnhandledRequest: 'error'](#9-not-setting-onunhandledrequest-error)
10. [Throwing errors inside resolvers](#10-throwing-errors-inside-resolvers)

## 1. Using v1 `rest` namespace

```typescript
// BAD
import { rest } from 'msw'
rest.get('/api/user', (req, res, ctx) => res(ctx.json({ name: 'John' })))

// GOOD
import { http, HttpResponse } from 'msw'
http.get('/api/user', () => HttpResponse.json({ name: 'John' }))
```

## 2. Putting query params in URL predicates

```typescript
// BAD: silently matches nothing
http.get('/post?id=1', resolver)

// GOOD: read query params inside resolver
http.get('/post', ({ request }) => {
  const url = new URL(request.url)
  const id = url.searchParams.get('id')
  return HttpResponse.json({ id })
})
```

## 3. Importing setupServer from wrong path

```typescript
// BAD
import { setupServer } from 'msw'

// GOOD
import { setupServer } from 'msw/node'
```

## 4. Missing afterEach(resetHandlers)

```typescript
// BAD: handlers leak between tests
beforeAll(() => server.listen())
afterAll(() => server.close())

// GOOD: reset after each test
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
```

## 5. Asserting on fetch calls

```typescript
// BAD: tests implementation, not behavior
expect(fetch).toHaveBeenCalledWith('/api/login', expect.objectContaining({
  method: 'POST',
}))

// GOOD: tests what the user sees
await waitFor(() => {
  expect(screen.getByText('Welcome!')).toBeInTheDocument()
})
```

## 6. Not awaiting request.json()

```typescript
// BAD: body is a ReadableStream, not parsed data
http.post('/api/user', ({ request }) => {
  const body = request.body
  return HttpResponse.json({ received: body })
})

// GOOD: await the async method
http.post('/api/user', async ({ request }) => {
  const body = await request.json()
  return HttpResponse.json({ received: body })
})
```

## 7. Using native Response for cookies

```typescript
// BAD: Set-Cookie silently dropped
new Response(null, {
  headers: { 'Set-Cookie': 'token=abc' },
})

// GOOD: HttpResponse supports Set-Cookie
new HttpResponse(null, {
  headers: { 'Set-Cookie': 'token=abc' },
})
```

## 8. Not using server.boundary() for concurrent tests

```typescript
// BAD: overrides leak across concurrent tests
it.concurrent('test A', async () => {
  server.use(http.get('/api/user', () => HttpResponse.json({ role: 'admin' })))
})

// GOOD: boundary isolates overrides
it.concurrent('test A', server.boundary(async () => {
  server.use(http.get('/api/user', () => HttpResponse.json({ role: 'admin' })))
}))
```

## 9. Not setting onUnhandledRequest: 'error'

```typescript
// BAD: unhandled requests silently pass through
server.listen()

// GOOD: missing handlers fail the test
server.listen({ onUnhandledRequest: 'error' })
```

## 10. Throwing errors inside resolvers

```typescript
// BAD: crashes handler internals
http.get('/api/data', () => {
  throw new Error('Network failure')
})

// GOOD: simulates actual network error
http.get('/api/data', () => {
  return HttpResponse.error()
})
```

```

### references/handler-api.md

```markdown
# Handler API Reference

## Table of Contents

- [HTTP Handlers](#http-handlers)
- [GraphQL Handlers](#graphql-handlers)
- [URL Predicates](#url-predicates)
- [Path Parameters](#path-parameters)
- [Handler Options](#handler-options)
- [matchRequestUrl Utility](#matchrequesturl-utility)

## HTTP Handlers

All methods on the `http` namespace:

| Method | Description |
|--------|-------------|
| `http.get(predicate, resolver, options?)` | GET requests |
| `http.post(predicate, resolver, options?)` | POST requests |
| `http.put(predicate, resolver, options?)` | PUT requests |
| `http.patch(predicate, resolver, options?)` | PATCH requests |
| `http.delete(predicate, resolver, options?)` | DELETE requests |
| `http.head(predicate, resolver, options?)` | HEAD requests |
| `http.options(predicate, resolver, options?)` | OPTIONS requests |
| `http.all(predicate, resolver, options?)` | Any HTTP method |

### Signature

```typescript
http.get(
  predicate: string | RegExp | URL | ((input: { request: Request }) => boolean),
  resolver: (info: {
    request: Request
    params: Record<string, string | string[]>
    cookies: Record<string, string>
    requestId: string
  }) => Response | HttpResponse | undefined | void | Promise<...>,
  options?: { once?: boolean }
)
```

## GraphQL Handlers

| Method | Description |
|--------|-------------|
| `graphql.query(operationName, resolver)` | GraphQL queries |
| `graphql.mutation(operationName, resolver)` | GraphQL mutations |
| `graphql.operation(resolver)` | Any GraphQL operation |
| `graphql.link(url)` | Scoped namespace for specific endpoint |

### GraphQL Resolver Info

```typescript
graphql.query('GetUser', ({
  query,           // DocumentNode — parsed GraphQL query
  variables,       // Record<string, any> — query variables
  operationName,   // string — operation name
  request,         // Request — standard Fetch API Request
  cookies,         // Record<string, string>
  requestId,       // string
}) => {
  return HttpResponse.json({ data: { user: { id: variables.id } } })
})
```

## URL Predicates

### String — Exact pathname match

```typescript
http.get('/api/user', resolver)
```

### String with params — Captures path parameters

```typescript
http.get('/api/user/:id', resolver)
```

### Wildcard — Matches path prefix

```typescript
http.get('/api/*', resolver) // matches /api/anything
```

### Absolute URL — Full URL match

```typescript
http.get('https://api.example.com/user', resolver)
```

### RegExp — Pattern matching

```typescript
http.get(/\/api\/user\/\d+/, resolver)
```

### Custom function — Programmatic matching

```typescript
http.get(
  ({ request }) => {
    const url = new URL(request.url)
    return url.pathname.startsWith('/api') && url.searchParams.has('v')
  },
  resolver,
)
```

## Path Parameters

### Single parameter

```typescript
http.get('/user/:id', ({ params }) => {
  const { id } = params // string
  return HttpResponse.json({ id })
})
```

### Multiple parameters

```typescript
http.get('/user/:userId/post/:postId', ({ params }) => {
  const { userId, postId } = params
  return HttpResponse.json({ userId, postId })
})
```

### Wildcard parameter

```typescript
http.get('/files/*', ({ params }) => {
  const path = params['*'] // everything after /files/
  return HttpResponse.json({ path })
})
```

## Handler Options

### One-time handler

Automatically removed after the first matching request:

```typescript
http.get('/api/data', resolver, { once: true })
```

Useful for testing retry logic — first request fails, second succeeds:

```typescript
server.use(
  http.get('/api/data', () => new HttpResponse(null, { status: 500 }), { once: true })
)
// First request → 500 (one-time handler consumed)
// Second request → default handler responds
```

## matchRequestUrl Utility

Utility for programmatic URL matching outside of handlers:

```typescript
import { matchRequestUrl } from 'msw'

const match = matchRequestUrl(
  new URL('https://example.com/user/abc-123'),
  '/user/:id',
)

if (match) {
  console.log(match.params.id) // 'abc-123'
}
```

```

### references/migration-v1-to-v2.md

```markdown
# Migration Guide: MSW v1 to v2

## Table of Contents

- [Import Changes](#import-changes)
- [Handler Namespace](#handler-namespace)
- [Resolver Signature](#resolver-signature)
- [Response Construction](#response-construction)
- [Request Property Changes](#request-property-changes)
- [GraphQL Changes](#graphql-changes)
- [Lifecycle Event Changes](#lifecycle-event-changes)
- [Removed APIs](#removed-apis)

## Import Changes

| v1 | v2 |
|----|-----|
| `import { rest } from 'msw'` | `import { http, HttpResponse } from 'msw'` |
| `import { setupServer } from 'msw/node'` | Same (unchanged) |
| `import { setupWorker } from 'msw'` | `import { setupWorker } from 'msw/browser'` |

```typescript
// BAD: v1 imports
import { rest } from 'msw'
import { setupWorker } from 'msw'

// GOOD: v2 imports
import { http, HttpResponse } from 'msw'
import { setupWorker } from 'msw/browser'
```

## Handler Namespace

The `rest` namespace is renamed to `http`:

| v1 | v2 |
|----|-----|
| `rest.get(url, resolver)` | `http.get(url, resolver)` |
| `rest.post(url, resolver)` | `http.post(url, resolver)` |
| `rest.put(url, resolver)` | `http.put(url, resolver)` |
| `rest.patch(url, resolver)` | `http.patch(url, resolver)` |
| `rest.delete(url, resolver)` | `http.delete(url, resolver)` |
| `rest.all(url, resolver)` | `http.all(url, resolver)` |

## Resolver Signature

The biggest breaking change. v1 used `(req, res, ctx)` three-argument resolvers. v2 uses a single object argument and returns a `Response` directly.

```typescript
// BAD: v1 resolver
rest.get('/user', (req, res, ctx) => {
  return res(ctx.json({ name: 'John' }))
})

// GOOD: v2 resolver
http.get('/user', ({ request, params, cookies }) => {
  return HttpResponse.json({ name: 'John' })
})
```

### v2 resolver info object

| Property | Type | Description |
|----------|------|-------------|
| `request` | `Request` | Standard Fetch API Request |
| `params` | `Record<string, string>` | URL path parameters |
| `cookies` | `Record<string, string>` | Parsed request cookies |
| `requestId` | `string` | Unique request identifier |

## Response Construction

Complete `ctx.*` to `HttpResponse.*` mapping:

| v1 | v2 |
|----|-----|
| `res(ctx.json(data))` | `HttpResponse.json(data)` |
| `res(ctx.text(str))` | `HttpResponse.text(str)` |
| `res(ctx.xml(str))` | `HttpResponse.xml(str)` |
| `res(ctx.body(str))` | `new HttpResponse(str)` |
| `res(ctx.status(code))` | `new HttpResponse(null, { status: code })` |
| `res(ctx.status(code), ctx.json(data))` | `HttpResponse.json(data, { status: code })` |
| `res(ctx.set(name, val))` | Include in `headers` init option |
| `res(ctx.cookie(name, val))` | `headers: { 'Set-Cookie': 'name=val' }` |
| `res(ctx.delay(ms), ctx.json(data))` | `await delay(ms); return HttpResponse.json(data)` |
| `res.once(ctx.json(data))` | `http.get(url, resolver, { once: true })` |
| `res.networkError(msg)` | `HttpResponse.error()` |
| `res(ctx.data({ user }))` | `HttpResponse.json({ data: { user } })` |
| `res(ctx.errors([...]))` | `HttpResponse.json({ errors: [...] })` |

### Full before/after examples

#### JSON response with status

```typescript
// BAD: v1
rest.post('/api/user', (req, res, ctx) => {
  return res(ctx.status(201), ctx.json({ id: '1', name: 'John' }))
})

// GOOD: v2
http.post('/api/user', () => {
  return HttpResponse.json({ id: '1', name: 'John' }, { status: 201 })
})
```

#### Custom headers

```typescript
// BAD: v1
rest.get('/api/data', (req, res, ctx) => {
  return res(ctx.set('X-Request-Id', '123'), ctx.json({ value: 42 }))
})

// GOOD: v2
http.get('/api/data', () => {
  return HttpResponse.json(
    { value: 42 },
    { headers: { 'X-Request-Id': '123' } },
  )
})
```

#### Delayed response

```typescript
// BAD: v1
rest.get('/api/user', (req, res, ctx) => {
  return res(ctx.delay(1000), ctx.json({ name: 'John' }))
})

// GOOD: v2
import { delay } from 'msw'

http.get('/api/user', async () => {
  await delay(1000)
  return HttpResponse.json({ name: 'John' })
})
```

#### One-time response

```typescript
// BAD: v1
rest.get('/api/data', (req, res, ctx) => {
  return res.once(ctx.json({ first: true }))
})

// GOOD: v2
http.get('/api/data', () => {
  return HttpResponse.json({ first: true })
}, { once: true })
```

#### Network error

```typescript
// BAD: v1
rest.get('/api/data', (req, res, ctx) => {
  return res.networkError('Failed to connect')
})

// GOOD: v2
http.get('/api/data', () => {
  return HttpResponse.error()
})
```

## Request Property Changes

| v1 (`req.*`) | v2 (`request` / resolver info) |
|-------------|-------------------------------|
| `req.url` | `new URL(request.url)` |
| `req.params` | `params` (from resolver info) |
| `req.cookies` | `cookies` (from resolver info) |
| `req.body` | `await request.json()` (async!) |
| `req.headers.get(name)` | `request.headers.get(name)` (same API) |

### Reading the request body is now async

```typescript
// BAD: v1 (sync body access)
rest.post('/api/user', (req, res, ctx) => {
  const { name } = req.body
  return res(ctx.json({ name }))
})

// GOOD: v2 (async body access)
http.post('/api/user', async ({ request }) => {
  const { name } = await request.json()
  return HttpResponse.json({ name })
})
```

### Reading URL and query params

```typescript
// BAD: v1
rest.get('/api/posts', (req, res, ctx) => {
  const page = req.url.searchParams.get('page')
  return res(ctx.json({ page }))
})

// GOOD: v2
http.get('/api/posts', ({ request }) => {
  const url = new URL(request.url)
  const page = url.searchParams.get('page')
  return HttpResponse.json({ page })
})
```

## GraphQL Changes

```typescript
// BAD: v1
graphql.query('GetUser', (req, res, ctx) => {
  const { id } = req.variables
  return res(ctx.data({ user: { id, name: 'John' } }))
})

// GOOD: v2
graphql.query('GetUser', ({ variables }) => {
  const { id } = variables
  return HttpResponse.json({
    data: { user: { id, name: 'John' } },
  })
})
```

| v1 | v2 |
|----|-----|
| `req.variables` | `variables` (from resolver info) |
| `req.operationName` | `operationName` (from resolver info) |
| `ctx.data(data)` | `HttpResponse.json({ data })` |
| `ctx.errors(errors)` | `HttpResponse.json({ errors })` |
| `ctx.extensions(ext)` | `HttpResponse.json({ extensions: ext })` |

## Lifecycle Event Changes

```typescript
// BAD: v1
server.on('request:start', (req) => {
  console.log(req.url.href)
})

// GOOD: v2
server.events.on('request:start', ({ request, requestId }) => {
  console.log(request.url)
})
```

Key changes:
- Access events through `server.events` instead of `server` directly
- Event payload is a destructurable object instead of positional arguments
- `request` is a standard Fetch API `Request` object

## Removed APIs

| Removed API | Replacement |
|-------------|-------------|
| `rest` namespace | `http` namespace |
| `res()` function | Return `HttpResponse` or `Response` directly |
| `ctx.*` helpers | `HttpResponse.*` static methods |
| `req.body` (sync) | `await request.json()` (async) |
| `req.params` | `params` from resolver info |
| `req.cookies` | `cookies` from resolver info |
| `res.once()` | `{ once: true }` handler option |
| `res.networkError()` | `HttpResponse.error()` |
| `ctx.fetch()` | Use native `fetch()` with `bypass()` from `msw` |

### ctx.fetch() to bypass()

```typescript
// BAD: v1
rest.get('/api/user', async (req, res, ctx) => {
  const originalResponse = await ctx.fetch(req)
  const body = await originalResponse.json()
  return res(ctx.json({ ...body, extra: true }))
})

// GOOD: v2
import { bypass } from 'msw'

http.get('/api/user', async ({ request }) => {
  const originalResponse = await fetch(bypass(request))
  const body = await originalResponse.json()
  return HttpResponse.json({ ...body, extra: true })
})
```

```

### references/response-api.md

```markdown
# Response API Reference

## Table of Contents

- [HttpResponse Class](#httpresponse-class)
- [Static Methods](#static-methods)
- [Response Init Options](#response-init-options)
- [Cookie Handling](#cookie-handling)
- [Comparison with Native Response](#comparison-with-native-response)

## HttpResponse Class

`HttpResponse` extends the native `Response` class. It can be used anywhere a `Response` is expected but adds support for forbidden headers like `Set-Cookie`.

```typescript
import { HttpResponse } from 'msw'
```

### Constructor

```typescript
new HttpResponse(body?, init?)
```

- `body` — `string | Blob | ArrayBuffer | ReadableStream | FormData | null`
- `init` — `{ status?, statusText?, headers? }`

```typescript
// Plain body with custom status
new HttpResponse('Not Found', { status: 404 })

// No body
new HttpResponse(null, { status: 204 })

// Streaming body
new HttpResponse(readableStream, {
  headers: { 'Content-Type': 'text/event-stream' },
})
```

## Static Methods

7 static methods for common response types:

| Method | Content-Type | Description |
|--------|-------------|-------------|
| `HttpResponse.json(body, init?)` | `application/json` | JSON response |
| `HttpResponse.text(body, init?)` | `text/plain` | Plain text |
| `HttpResponse.html(body, init?)` | `text/html` | HTML content |
| `HttpResponse.xml(body, init?)` | `text/xml` | XML content |
| `HttpResponse.formData(body, init?)` | `multipart/form-data` | Form data |
| `HttpResponse.arrayBuffer(body, init?)` | (none) | Binary data |
| `HttpResponse.error()` | (network error) | Network failure |

### HttpResponse.json()

```typescript
HttpResponse.json({ name: 'John', age: 30 })
HttpResponse.json({ name: 'John' }, { status: 201 })
HttpResponse.json({ error: 'Not found' }, { status: 404 })
HttpResponse.json([{ id: 1 }, { id: 2 }]) // arrays work too
```

### HttpResponse.text()

```typescript
HttpResponse.text('Hello, world!')
HttpResponse.text('Created', { status: 201 })
```

### HttpResponse.html()

```typescript
HttpResponse.html('<h1>Hello</h1>')
HttpResponse.html('<!DOCTYPE html><html><body>Page</body></html>')
```

### HttpResponse.xml()

```typescript
HttpResponse.xml('<user><name>John</name></user>')
```

### HttpResponse.formData()

```typescript
const form = new FormData()
form.append('name', 'John')
form.append('avatar', new Blob(['...'], { type: 'image/png' }))
HttpResponse.formData(form)
```

### HttpResponse.arrayBuffer()

```typescript
const buffer = new ArrayBuffer(8)
HttpResponse.arrayBuffer(buffer)
```

### HttpResponse.error()

Creates a network error response (`type: "error"`). The client receives a `TypeError: Failed to fetch`.

```typescript
HttpResponse.error()
// No arguments — network errors have no body, status, or headers
```

## Response Init Options

All static methods and the constructor accept an optional init object:

```typescript
HttpResponse.json(
  { name: 'John' },
  {
    status: 201,
    statusText: 'Created',
    headers: {
      'X-Custom-Header': 'value',
      'Set-Cookie': 'token=abc; Path=/; HttpOnly',
    },
  },
)
```

### Multiple headers with same name

```typescript
new HttpResponse(null, {
  headers: [
    ['Set-Cookie', 'session=abc'],
    ['Set-Cookie', 'theme=dark'],
  ],
})
```

## Cookie Handling

Native `Response` forbids `Set-Cookie` in the constructor. `HttpResponse` bypasses this:

```typescript
// GOOD: HttpResponse supports Set-Cookie
new HttpResponse(null, {
  headers: { 'Set-Cookie': 'session=abc123' },
})

// BAD: native Response silently drops Set-Cookie
new Response(null, {
  headers: { 'Set-Cookie': 'session=abc123' },
})
```

### Multiple cookies

```typescript
new HttpResponse(null, {
  headers: [
    ['Set-Cookie', 'session=abc123; Path=/; HttpOnly'],
    ['Set-Cookie', 'theme=dark; Path=/'],
  ],
})
```

## Comparison with Native Response

| Feature | `Response` | `HttpResponse` |
|---------|-----------|----------------|
| JSON body | Manual `JSON.stringify` | `HttpResponse.json()` auto-serializes |
| Content-Type | Must set manually | Auto-set by static methods |
| Set-Cookie | Forbidden (silently dropped) | Supported |
| Network error | Not possible | `HttpResponse.error()` |
| Use in MSW handlers | Yes (except cookies) | Yes (full support) |

```

### references/server-api.md

```markdown
# Server API Reference

## Table of Contents

- [setupServer](#setupserver-nodejs)
- [setupWorker](#setupworker-browser)
- [Server Methods](#server-methods)
- [Worker Methods](#worker-methods)
- [Lifecycle Events](#lifecycle-events)
- [onUnhandledRequest Strategies](#onunhandledrequest-strategies)
- [server.boundary()](#serverboundary)

## setupServer (Node.js)

```typescript
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

const server = setupServer(...handlers)
```

Used for Node.js environments: test runners (Vitest, Jest), scripts, and server-side applications.

## setupWorker (Browser)

```typescript
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

const worker = setupWorker(...handlers)
```

Used for browser environments: development servers, Storybook, browser-based tests.

## Server Methods

| Method | Description |
|--------|-------------|
| `.listen(options?)` | Start intercepting requests |
| `.close()` | Stop intercepting, clean up |
| `.use(...handlers)` | Prepend runtime handlers (override) |
| `.resetHandlers(...handlers?)` | Remove runtime handlers, optionally replace initial handlers |
| `.restoreHandlers()` | Mark used one-time handlers as unused again |
| `.listHandlers()` | Return array of all active handlers |
| `.boundary(callback)` | Create isolated handler scope for concurrent tests |
| `.events` | EventEmitter for lifecycle events |

### .listen()

```typescript
server.listen()

// With options
server.listen({
  onUnhandledRequest: 'error',
})
```

### .close()

```typescript
server.close()
```

### .use()

Prepends runtime handlers that take priority over initial handlers:

```typescript
server.use(
  http.get('/api/user', () => HttpResponse.json({ name: 'Override' }))
)
```

Runtime handlers are checked first (most recently added wins). They are removed by `resetHandlers()`.

### .resetHandlers()

```typescript
// Remove all runtime handlers, keep initial handlers
server.resetHandlers()

// Replace initial handlers entirely
server.resetHandlers(
  http.get('/api/user', () => HttpResponse.json({ name: 'New default' }))
)
```

### .restoreHandlers()

Restores one-time handlers that have already been used so they can fire again:

```typescript
server.restoreHandlers()
```

### .listHandlers()

```typescript
const handlers = server.listHandlers()
console.log(handlers.length)
```

## Worker Methods

| Method | Description |
|--------|-------------|
| `.start(options?)` | Register service worker, start intercepting |
| `.stop()` | Unregister service worker, stop intercepting |
| `.use(...handlers)` | Same as server |
| `.resetHandlers()` | Same as server |
| `.restoreHandlers()` | Same as server |
| `.listHandlers()` | Same as server |

### .start()

```typescript
await worker.start()

// With options
await worker.start({
  onUnhandledRequest: 'error',
  serviceWorker: {
    url: '/mockServiceWorker.js',
  },
  quiet: true, // suppress "[MSW] Mocking enabled" console message
})
```

### .stop()

```typescript
worker.stop()
```

## Lifecycle Events

7 event types available on `server.events` / `worker.events`:

| Event | Payload | Description |
|-------|---------|-------------|
| `request:start` | `{ request, requestId }` | Request intercepted |
| `request:match` | `{ request, requestId }` | Handler found |
| `request:unhandled` | `{ request, requestId }` | No handler found |
| `request:end` | `{ request, requestId }` | Request processing done |
| `response:mocked` | `{ request, requestId, response }` | Mocked response sent |
| `response:bypass` | `{ request, requestId, response }` | Real response received |
| `unhandledException` | `{ request, requestId, error }` | Handler threw error |

### Subscribing to events

```typescript
server.events.on('request:start', ({ request, requestId }) => {
  console.log('Intercepted:', request.method, request.url)
})

server.events.on('response:mocked', ({ request, response }) => {
  console.log('Mocked:', request.url, response.status)
})

server.events.on('unhandledException', ({ request, error }) => {
  console.error('Handler error for', request.url, error)
})
```

### Clone before reading body

```typescript
// BAD: consumes the request body, breaking downstream handlers
server.events.on('request:start', async ({ request }) => {
  const body = await request.json()
})

// GOOD: clone before reading
server.events.on('request:start', async ({ request }) => {
  const body = await request.clone().json()
})
```

### Removing event listeners

```typescript
const listener = ({ request }) => {
  console.log(request.url)
}

server.events.on('request:start', listener)
server.events.removeListener('request:start', listener)

// Or remove all listeners
server.events.removeAllListeners()
```

## onUnhandledRequest Strategies

| Strategy | Behavior |
|----------|----------|
| `'warn'` (default) | Console warning, request passes through |
| `'error'` | Throws error, fails test |
| `'bypass'` | Silent, request passes through |
| Custom function | Conditional handling |

### Built-in strategies

```typescript
server.listen({ onUnhandledRequest: 'warn' })   // default
server.listen({ onUnhandledRequest: 'error' })   // recommended for tests
server.listen({ onUnhandledRequest: 'bypass' })  // silent passthrough
```

### Custom strategy

```typescript
server.listen({
  onUnhandledRequest(request, print) {
    const url = new URL(request.url)

    // Ignore specific paths
    if (url.pathname.startsWith('/assets/')) {
      return
    }

    // Ignore specific hosts
    if (url.hostname === 'cdn.example.com') {
      return
    }

    // Error on everything else
    print.error()
  },
})
```

## server.boundary()

Isolates handler scope for parallel execution. Handlers added via `server.use()` inside a boundary are only visible within that boundary's execution context.

```typescript
const isolated = server.boundary(async () => {
  server.use(
    http.get('/api/user', () => HttpResponse.json({ role: 'admin' }))
  )
  // This override is only visible within this boundary
})

await isolated()
```

### Use with concurrent tests

```typescript
it.concurrent('admin flow', server.boundary(async () => {
  server.use(
    http.get('/api/me', () => HttpResponse.json({ role: 'admin' }))
  )
  // Only this test sees the admin override
}))

it.concurrent('guest flow', server.boundary(async () => {
  server.use(
    http.get('/api/me', () => HttpResponse.json({ role: 'guest' }))
  )
  // Only this test sees the guest override
}))
```

```

### references/test-patterns.md

```markdown
# Test Patterns Reference

## Table of Contents

- [Setup for Vitest](#setup-for-vitest)
- [Setup for Jest](#setup-for-jest)
- [Per-Test Overrides](#per-test-overrides)
- [Concurrent Test Isolation](#concurrent-test-isolation)
- [Handler Organization](#handler-organization)
- [Higher-Order Resolvers](#higher-order-resolvers)
- [Dynamic Mock Scenarios](#dynamic-mock-scenarios)
- [Cache Clearing for React Query / SWR / Apollo](#cache-clearing)
- [Conditional Browser Mocking](#conditional-browser-mocking)

## Setup for Vitest

```typescript
// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest'
import { server } from './src/mocks/node'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
```

```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    setupFiles: ['./vitest.setup.ts'],
  },
})
```

## Setup for Jest

```typescript
// jest.setup.ts
import { server } from './src/mocks/node'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
```

```javascript
// jest.config.js
module.exports = {
  setupFilesAfterEnv: ['./jest.setup.ts'],
}
```

## Per-Test Overrides

Use `server.use()` to add handlers that only last until `resetHandlers()` runs in `afterEach`.

### Override for error state

```typescript
test('shows error when API fails', async () => {
  server.use(
    http.get('/api/user', () => {
      return new HttpResponse(null, { status: 500 })
    })
  )

  render(<UserProfile />)
  await waitFor(() => {
    expect(screen.getByText('Something went wrong')).toBeInTheDocument()
  })
})
```

### Override for empty state

```typescript
test('shows empty state', async () => {
  server.use(
    http.get('/api/posts', () => HttpResponse.json([]))
  )

  render(<PostList />)
  await waitFor(() => {
    expect(screen.getByText('No posts yet')).toBeInTheDocument()
  })
})
```

### Override for slow response (loading state)

```typescript
import { delay, http, HttpResponse } from 'msw'

test('shows loading state', async () => {
  server.use(
    http.get('/api/user', async () => {
      await delay('infinite')
      return HttpResponse.json({ name: 'John' })
    })
  )

  render(<UserProfile />)
  expect(screen.getByText('Loading...')).toBeInTheDocument()
})
```

### One-time override for retry testing

```typescript
test('retries after failure', async () => {
  server.use(
    // First request fails, then handler is consumed
    http.get('/api/data', () => {
      return new HttpResponse(null, { status: 500 })
    }, { once: true })
  )
  // After one-time handler consumed, default handler responds with success

  render(<DataLoader />)
  await waitFor(() => {
    expect(screen.getByText('Data loaded')).toBeInTheDocument()
  })
})
```

## Concurrent Test Isolation

Use `server.boundary()` to prevent handler leakage between concurrent tests:

```typescript
it.concurrent('admin flow', server.boundary(async () => {
  server.use(
    http.get('/api/me', () => HttpResponse.json({ role: 'admin' }))
  )
  // Only this test sees the admin override
}))

it.concurrent('guest flow', server.boundary(async () => {
  server.use(
    http.get('/api/me', () => HttpResponse.json({ role: 'guest' }))
  )
  // Only this test sees the guest override
}))
```

## Handler Organization

Recommended directory structure:

```
src/mocks/
├── handlers.ts          # Aggregates and exports all handlers
├── handlers/
│   ├── user.ts          # User-related handlers
│   ├── posts.ts         # Post-related handlers
│   └── auth.ts          # Auth-related handlers
├── node.ts              # setupServer(...handlers)
└── browser.ts           # setupWorker(...handlers)
```

### handlers/user.ts

```typescript
import { http, HttpResponse } from 'msw'

export const userHandlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: '1', name: 'John' },
      { id: '2', name: 'Jane' },
    ])
  }),

  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: params.id, name: 'John' })
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json(body, { status: 201 })
  }),
]
```

### handlers.ts

```typescript
import { userHandlers } from './handlers/user'
import { postHandlers } from './handlers/posts'
import { authHandlers } from './handlers/auth'

export const handlers = [
  ...authHandlers,
  ...userHandlers,
  ...postHandlers,
]
```

## Higher-Order Resolvers

Factory functions for reusable response patterns:

### Authentication wrapper

```typescript
import { http, HttpResponse } from 'msw'

function withAuth(resolver) {
  return async (info) => {
    const token = info.request.headers.get('Authorization')
    if (!token) {
      return new HttpResponse(null, { status: 401 })
    }
    return resolver(info)
  }
}

// Usage
http.get('/api/profile', withAuth(({ request }) => {
  return HttpResponse.json({ name: 'John' })
}))
```

### Paginated response wrapper

```typescript
function withPagination(items) {
  return ({ request }) => {
    const url = new URL(request.url)
    const page = Number(url.searchParams.get('page') ?? '1')
    const limit = Number(url.searchParams.get('limit') ?? '10')
    const start = (page - 1) * limit
    const paginatedItems = items.slice(start, start + limit)

    return HttpResponse.json({
      data: paginatedItems,
      total: items.length,
      page,
      totalPages: Math.ceil(items.length / limit),
    })
  }
}

http.get('/api/posts', withPagination(allPosts))
```

## Dynamic Mock Scenarios

Create scenario-specific handler sets for reuse across tests:

```typescript
function createUserHandlers(scenario: 'happy' | 'error' | 'empty') {
  switch (scenario) {
    case 'happy':
      return http.get('/api/user', () =>
        HttpResponse.json({ name: 'John', email: '[email protected]' }))
    case 'error':
      return http.get('/api/user', () =>
        new HttpResponse(null, { status: 500 }))
    case 'empty':
      return http.get('/api/user', () =>
        new HttpResponse(null, { status: 404 }))
  }
}

test('error scenario', async () => {
  server.use(createUserHandlers('error'))
  // ...
})
```

### Multi-endpoint scenario

```typescript
function createScenario(scenario: 'authenticated' | 'anonymous') {
  if (scenario === 'authenticated') {
    return [
      http.get('/api/me', () => HttpResponse.json({ id: '1', name: 'John' })),
      http.get('/api/settings', () => HttpResponse.json({ theme: 'dark' })),
    ]
  }
  return [
    http.get('/api/me', () => new HttpResponse(null, { status: 401 })),
  ]
}

test('authenticated dashboard', async () => {
  server.use(...createScenario('authenticated'))
  // ...
})
```

## Cache Clearing

### React Query

Create a fresh `QueryClient` per test to avoid stale cache:

```typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function renderWithClient(ui: React.ReactElement) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  })
  return render(
    <QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
  )
}
```

### SWR

Use `SWRConfig` with a fresh cache per test:

```typescript
import { SWRConfig } from 'swr'

function renderWithSWR(ui: React.ReactElement) {
  return render(
    <SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>
      {ui}
    </SWRConfig>
  )
}
```

### Apollo Client

Create a fresh `InMemoryCache` per test:

```typescript
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'

function renderWithApollo(ui: React.ReactElement) {
  const client = new ApolloClient({
    uri: '/graphql',
    cache: new InMemoryCache(),
  })
  return render(
    <ApolloProvider client={client}>{ui}</ApolloProvider>
  )
}
```

## Conditional Browser Mocking

Enable mocking only in development:

```typescript
// src/main.tsx
async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') {
    return
  }
  const { worker } = await import('./mocks/browser')
  return worker.start({ onUnhandledRequest: 'bypass' })
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
})
```

### In Storybook

```typescript
// .storybook/preview.ts
import { initialize, mswLoader } from 'msw-storybook-addon'
import { handlers } from '../src/mocks/handlers'

initialize({ onUnhandledRequest: 'bypass' })

export const parameters = {
  msw: { handlers },
}

export const loaders = [mswLoader]
```

```

msw | SkillHub