
Msw
- 578 installs
- 186 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
msw is a Claude Code skill that generates and maintains Mock Service Worker v2 setups, handlers, and debugging patterns so developers eliminate flaky API tests and unhandled request errors.
About
msw is a pproenca dot-skills guide at version 1.0.0 for mswjs Mock Service Worker v2, packing 45+ rules across 8 categories from critical setup and handler architecture through incremental debugging guidance. Each rule pairs incorrect versus correct examples with impact notes so agents scaffold browser and Node interceptors, REST and GraphQL handlers, and error paths consistently. Developers reach for msw when standing up API mocks for React or Vitest suites, refactoring legacy MSW v1 handlers, or stopping unhandled-request failures during CI component tests.
- 45+ prioritized rules across 8 categories
- Critical setup guidance including committing the worker script and configuring lifecycle hooks
- Real-world incorrect vs correct code examples with impact metrics
- TypeScript v2 configuration and unhandled request behavior rules
- Designed specifically for AI agents and automated refactoring workflows
Msw by the numbers
- 578 all-time installs (skills.sh)
- Ranked #598 of 2,159 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill mswAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 578 |
|---|---|
| repo stars | ★ 186 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do you set up MSW v2 handlers for API tests?
Generate consistent, production-grade Mock Service Worker setups and handlers that eliminate flaky tests and unhandled request errors.
Who is it for?
Frontend and full-stack developers standardizing Mock Service Worker v2 across Vitest, Playwright, or Storybook test suites.
Skip if: Backend-only integration tests hitting real databases, projects already on MSW v1 without a migration plan, or teams mocking exclusively with static JSON fixtures.
When should I use this skill?
A developer asks to mock APIs with MSW, fix unhandled request errors, or generate handlers for frontend tests.
What you get
MSW v2 worker/bootstrap files, typed HTTP handlers, and test configurations without unhandled-request or flaky mock errors.
- MSW worker setup files
- HTTP handler modules
- Test-ready mock configurations
By the numbers
- Contains 45+ rules across 8 categories
- Skill version 1.0.0 for MSW v2 (mswjs), January 2026
Files
MSW Best Practices
Comprehensive API mocking guide for MSW v2 applications, designed for AI agents and LLMs. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
When to Apply
Reference these guidelines when:
- Setting up MSW for testing or development
- Writing or organizing request handlers
- Configuring test environments with MSW
- Mocking REST or GraphQL APIs
- Debugging handler matching issues
- Testing error states and edge cases
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Setup & Initialization | CRITICAL | setup- |
| 2 | Handler Architecture | CRITICAL | handler- |
| 3 | Test Integration | HIGH | test- |
| 4 | Response Patterns | HIGH | response- |
| 5 | Request Matching | MEDIUM-HIGH | match- |
| 6 | GraphQL Mocking | MEDIUM | graphql- |
| 7 | Advanced Patterns | MEDIUM | advanced- |
| 8 | Debugging & Performance | LOW | debug- |
Quick Reference
1. Setup & Initialization (CRITICAL)
setup-server-node-entrypoint- Use correct entrypoint for Node.js (msw/node)setup-lifecycle-hooks- Configure server lifecycle in test setupsetup-worker-script-commit- Commit worker script to version controlsetup-node-version- Require Node.js 18+ for MSW v2setup-unhandled-requests- Configure unhandled request behaviorsetup-typescript-config- Configure TypeScript for MSW v2
2. Handler Architecture (CRITICAL)
handler-happy-path-first- Define happy path handlers as baselinehandler-domain-grouping- Group handlers by domainhandler-absolute-urls- Use absolute URLs in handlershandler-shared-resolvers- Extract shared response logic into resolvershandler-v2-response-syntax- Use MSW v2 response syntaxhandler-request-body-parsing- Explicitly parse request bodieshandler-resolver-argument- Destructure resolver arguments correctlyhandler-reusability-environments- Share handlers across environments
3. Test Integration (HIGH)
test-reset-handlers- Reset handlers after each testtest-avoid-request-assertions- Avoid direct request assertionstest-concurrent-boundary- Use server.boundary() for concurrent teststest-fake-timers-config- Configure fake timers to preserve queueMicrotasktest-async-utilities- Use async testing utilities for mock responsestest-clear-request-cache- Clear request library caches between teststest-jsdom-environment- Use correct JSDOM environment for Jest
4. Response Patterns (HIGH)
response-http-response-helpers- Use HttpResponse static methodsresponse-delay-realistic- Add realistic response delaysresponse-error-simulation- Simulate error responses correctlyresponse-one-time-handlers- Use one-time handlers for sequential scenariosresponse-custom-headers- Set response headers correctlyresponse-streaming- Mock streaming responses with ReadableStream
5. Request Matching (MEDIUM-HIGH)
match-url-patterns- Use URL path parameters correctlymatch-query-params- Access query parameters from request URLmatch-custom-predicate- Use custom predicates for complex matchingmatch-http-methods- Match HTTP methods explicitlymatch-handler-order- Order handlers from specific to general
6. GraphQL Mocking (MEDIUM)
graphql-operation-handlers- Use operation name for GraphQL matchinggraphql-error-responses- Return GraphQL errors in correct formatgraphql-batched-queries- Handle batched GraphQL queriesgraphql-variables-access- Access GraphQL variables correctly
7. Advanced Patterns (MEDIUM)
advanced-bypass-requests- Use bypass() for passthrough requestsadvanced-cookies-auth- Handle cookies and authenticationadvanced-dynamic-scenarios- Implement dynamic mock scenariosadvanced-vitest-browser- Configure MSW for Vitest browser modeadvanced-file-uploads- Mock file upload endpoints
8. Debugging & Performance (LOW)
debug-lifecycle-events- Use lifecycle events for debuggingdebug-verify-interception- Verify request interception is workingdebug-common-issues- Know common MSW issues and fixesdebug-request-logging- Log request details for debugging
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
- Individual rules:
references/{prefix}-{slug}.md
Related Skills
- For generating MSW mocks from OpenAPI, see
orvalskill - For consuming mocked APIs, see
tanstack-queryskill - For test methodology, see
test-vitestortest-tddskills
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md
MSW (Mock Service Worker)
Version 1.0.0 mswjs January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases. Humans may also find it useful,
but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Comprehensive API mocking guide for MSW v2 applications, designed for AI agents and LLMs. Contains 45+ rules across 8 categories, prioritized by impact from critical (setup, handler architecture) to incremental (debugging). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
---
Table of Contents
1. Setup & Initialization — CRITICAL
- 1.1 Commit Worker Script to Version Control — CRITICAL (Eliminates setup friction for team members; prevents CI failures)
- 1.2 Configure Server Lifecycle in Test Setup — CRITICAL (Prevents handler leakage and ensures test isolation; eliminates flaky tests)
- 1.3 Configure TypeScript for MSW v2 — CRITICAL (TypeScript 4.7+ required; incorrect config causes type errors)
- 1.4 Configure Unhandled Request Behavior — CRITICAL (Catches missing handlers immediately; prevents silent test failures)
- 1.5 Require Node.js 18+ for MSW v2 — CRITICAL (MSW v2 requires Node 18+; older versions cause complete failure)
- 1.6 Use Correct Entrypoint for Node.js — CRITICAL (Zero mocking if wrong entrypoint; 100% test failures)
2. Handler Architecture — CRITICAL
- 2.1 Define Happy Path Handlers as Baseline — CRITICAL (Establishes reliable baseline; enables clean runtime overrides)
- 2.2 Destructure Resolver Arguments Correctly — CRITICAL (Wrong destructuring pattern causes undefined values; silent failures)
- 2.3 Explicitly Parse Request Bodies — CRITICAL (v2 no longer auto-parses bodies; missing parsing returns undefined)
- 2.4 Extract Shared Response Logic into Resolvers — CRITICAL (Eliminates duplication; ensures consistent mock responses across tests)
- 2.5 Group Handlers by Domain — CRITICAL (Reduces maintenance overhead; scales to large APIs without N×M complexity)
- 2.6 Share Handlers Across Environments — CRITICAL (Single source of truth; eliminates mock drift between dev/test)
- 2.7 Use Absolute URLs in Handlers — CRITICAL (Prevents URL mismatch failures; required for Node.js environments)
- 2.8 Use MSW v2 Response Syntax — CRITICAL (v1 syntax breaks in v2; causes complete handler failure)
3. Test Integration — HIGH
- 3.1 Avoid Direct Request Assertions — HIGH (Tests implementation details; breaks on refactors that preserve behavior)
- 3.2 Clear Request Library Caches Between Tests — HIGH (Prevents stale cached responses; ensures fresh mock data per test)
- 3.3 Configure Fake Timers to Preserve queueMicrotask — HIGH (Prevents request body parsing from hanging indefinitely)
- 3.4 Reset Handlers After Each Test — HIGH (Prevents handler pollution; eliminates test order dependencies)
- 3.5 Use Async Testing Utilities for Mock Responses — HIGH (Prevents race conditions; ensures responses arrive before assertions)
- 3.6 Use Correct JSDOM Environment for Jest — HIGH (Prevents Node.js global conflicts; ensures proper fetch availability)
- 3.7 Use server.boundary() for Concurrent Tests — HIGH (Enables parallel test execution; prevents cross-test handler pollution)
4. Response Patterns — HIGH
- 4.1 Add Realistic Response Delays — HIGH (Reveals race conditions; tests loading states; catches timing bugs)
- 4.2 Mock Streaming Responses with ReadableStream — HIGH (Tests streaming UIs, chat interfaces, and progressive loading)
- 4.3 Set Response Headers Correctly — HIGH (Ensures CORS, caching, and authentication headers work as expected)
- 4.4 Simulate Error Responses Correctly — HIGH (Validates error handling; catches missing error states in UI)
- 4.5 Use HttpResponse Static Methods — HIGH (Automatic Content-Type headers; cleaner syntax; type safety)
- 4.6 Use One-Time Handlers for Sequential Scenarios — HIGH (Models realistic multi-step flows; tests retry logic correctly)
5. Request Matching — MEDIUM-HIGH
- 5.1 Access Query Parameters from Request URL — MEDIUM-HIGH (Enables filtering, pagination, and search mocking)
- 5.2 Match HTTP Methods Explicitly — MEDIUM-HIGH (Prevents cross-method interference; models REST APIs correctly)
- 5.3 Order Handlers from Specific to General — MEDIUM-HIGH (Prevents general handlers from shadowing specific ones)
- 5.4 Use Custom Predicates for Complex Matching — MEDIUM-HIGH (Enables header-based, body-based, and conditional request matching)
- 5.5 Use URL Path Parameters Correctly — MEDIUM-HIGH (Prevents silent handler mismatches; enables dynamic URL matching)
6. GraphQL Mocking — MEDIUM
- 6.1 Access GraphQL Variables Correctly — MEDIUM (Enables dynamic mock responses based on query input)
- 6.2 Handle Batched GraphQL Queries — MEDIUM (Supports Apollo batching; prevents unhandled batch requests)
- 6.3 Return GraphQL Errors in Correct Format — MEDIUM (Ensures GraphQL clients parse errors correctly; tests error handling)
- 6.4 Use Operation Name for GraphQL Matching — MEDIUM (Enables precise operation targeting; prevents query/mutation conflicts)
7. Advanced Patterns — MEDIUM
- 7.1 Configure MSW for Vitest Browser Mode — MEDIUM (Enables browser-environment testing with proper worker setup)
- 7.2 Handle Cookies and Authentication — MEDIUM (Enables session-based auth testing; validates auth flows)
- 7.3 Implement Dynamic Mock Scenarios — MEDIUM (Enables runtime mock state changes; supports complex test flows)
- 7.4 Mock File Upload Endpoints — MEDIUM (Tests file upload forms and progress indicators)
- 7.5 Use bypass() for Passthrough Requests — MEDIUM (Enables mixing real and mocked APIs; supports hybrid testing)
8. Debugging & Performance — LOW
- 8.1 Know Common MSW Issues and Fixes — LOW (Quick reference for frequent problems; reduces debugging time)
- 8.2 Log Request Details for Debugging — LOW (Provides detailed request inspection; identifies payload issues)
- 8.3 Use Lifecycle Events for Debugging — LOW (Provides visibility into request interception; aids troubleshooting)
- 8.4 Verify Request Interception is Working — LOW (Confirms MSW is active; identifies setup failures early)
---
References
1. https://mswjs.io/docs/ 2. https://mswjs.io/docs/best-practices/ 3. https://mswjs.io/docs/migrations/1.x-to-2.x/ 4. https://mswjs.io/docs/runbook/ 5. https://github.com/mswjs/msw
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
Rule Title Here
Brief explanation of WHY this matters (1-3 sentences focusing on performance/reliability implications).
Incorrect (what's wrong):
// Bad code example with comments explaining the costCorrect (what's right):
// Good code example with comments explaining the benefitWhen NOT to use this pattern:
- Exception 1
- Exception 2
Reference: Reference Title
{
"version": "1.0.1",
"organization": "mswjs",
"technology": "MSW (Mock Service Worker)",
"date": "January 2026",
"abstract": "Comprehensive API mocking guide for MSW v2 applications, designed for AI agents and LLMs. Contains 45+ rules across 8 categories, prioritized by impact from critical (setup, handler architecture) to incremental (debugging). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://mswjs.io/docs/",
"https://mswjs.io/docs/best-practices/",
"https://mswjs.io/docs/migrations/1.x-to-2.x/",
"https://mswjs.io/docs/runbook/",
"https://github.com/mswjs/msw"
],
"category": "Testing"
}
MSW Best Practices
Comprehensive API mocking guide for MSW (Mock Service Worker) v2 applications. This skill helps you set up MSW correctly, write effective request handlers, and integrate with test frameworks.
Overview
This skill contains 45 rules across 8 categories, organized by impact level:
| Category | Rules | Impact |
|---|---|---|
| Setup & Initialization | 6 | CRITICAL |
| Handler Architecture | 8 | CRITICAL |
| Test Integration | 7 | HIGH |
| Response Patterns | 6 | HIGH |
| Request Matching | 5 | MEDIUM-HIGH |
| GraphQL Mocking | 4 | MEDIUM |
| Advanced Patterns | 5 | MEDIUM |
| Debugging & Performance | 4 | LOW |
Structure
mswjs/
├── SKILL.md # Entry point with quick reference
├── AGENTS.md # Compiled comprehensive guide
├── metadata.json # Version, references, metadata
├── README.md # This file
├── references/
│ ├── _sections.md # Category definitions
│ ├── setup-*.md # Setup rules (6)
│ ├── handler-*.md # Handler architecture rules (8)
│ ├── test-*.md # Test integration rules (7)
│ ├── response-*.md # Response pattern rules (6)
│ ├── match-*.md # Request matching rules (5)
│ ├── graphql-*.md # GraphQL mocking rules (4)
│ ├── advanced-*.md # Advanced pattern rules (5)
│ └── debug-*.md # Debugging rules (4)
└── assets/
└── templates/
└── _template.md # Rule templateGetting Started
Using in Claude Code
This skill automatically activates when you're working on:
- MSW handler files (
handlers.ts,mocks/*.ts) - Test setup files that configure MSW
- API mocking patterns in tests
- REST or GraphQL API mocking
Manual Commands
# Install dependencies (if contributing)
pnpm install
# Build AGENTS.md from rules
pnpm build
# Validate skill structure
pnpm validateCreating a New Rule
1. Determine the category based on the rule's primary concern 2. Use the appropriate prefix from the table below 3. Copy assets/templates/_template.md as your starting point 4. Fill in frontmatter and content
Prefix Reference
| Prefix | Category | Impact |
|---|---|---|
setup- | Setup & Initialization | CRITICAL |
handler- | Handler Architecture | CRITICAL |
test- | Test Integration | HIGH |
response- | Response Patterns | HIGH |
match- | Request Matching | MEDIUM-HIGH |
graphql- | GraphQL Mocking | MEDIUM |
advanced- | Advanced Patterns | MEDIUM |
debug- | Debugging & Performance | LOW |
Rule File Structure
Each rule follows this template:
---
title: Rule Title Here
impact: CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW
impactDescription: Quantified impact (e.g., "2-10× improvement")
tags: prefix, technique, related-concepts
---
## Rule Title Here
1-3 sentences explaining WHY this matters.
**Incorrect (what's wrong):**
\`\`\`typescript
// Bad example
\`\`\`
**Correct (what's right):**
\`\`\`typescript
// Good example
\`\`\`
Reference: [Link](https://example.com)File Naming Convention
Rule files follow the pattern: {prefix}-{description}.md
Examples:
setup-server-node-entrypoint.md- Setup category, about Node.js entrypointhandler-happy-path-first.md- Handler category, about handler organizationtest-reset-handlers.md- Test category, about resetting handlers
Impact Levels
| Level | Description |
|---|---|
| CRITICAL | MSW fails to function without this |
| HIGH | Major degradation in test reliability |
| MEDIUM-HIGH | Significant impact on specific workflows |
| MEDIUM | Noticeable improvement in quality |
| LOW-MEDIUM | Incremental improvement |
| LOW | Minor optimization |
Scripts
| Command | Description |
|---|---|
pnpm build | Compiles rules into AGENTS.md |
pnpm validate | Validates skill structure and rules |
Contributing
1. Check existing rules to avoid duplication 2. Use the rule template (assets/templates/_template.md) 3. Include both incorrect and correct examples 4. Quantify impact where possible 5. Reference authoritative sources 6. Run validation before submitting
Acknowledgments
This skill draws from:
License
Apache 2.0
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Setup & Initialization (setup)
Impact: CRITICAL Description: Worker and server configuration determines all downstream request interception; misconfiguration results in zero mocking capability and silent test failures.
2. Handler Architecture (handler)
Impact: CRITICAL Description: Handler structure affects maintainability, match reliability, and reusability across environments; poor organization creates N×M complexity as endpoints grow.
3. Test Integration (test)
Impact: HIGH Description: Improper test setup causes flaky tests, isolation failures, handler leakage between tests, and false positives that mask real bugs.
4. Response Patterns (response)
Impact: HIGH Description: Response construction patterns affect type safety, realism, and consistency with production APIs; incorrect responses create false confidence in tests.
5. Request Matching (match)
Impact: MEDIUM-HIGH Description: Predicate accuracy determines handler activation; subtle URL mismatches cause silent handler failures that are difficult to debug.
6. GraphQL Mocking (graphql)
Impact: MEDIUM Description: GraphQL-specific patterns for intercepting operations, handling variables, query batching, and error simulation require dedicated approaches.
7. Advanced Patterns (advanced)
Impact: MEDIUM Description: Complex scenarios including request bypass, passthrough, cookies, authentication, streaming, and WebSocket mocking for comprehensive API simulation.
8. Debugging & Performance (debug)
Impact: LOW Description: Observability tools, lifecycle events, and troubleshooting patterns for diagnosing MSW configuration issues during development.
Use bypass() for Passthrough Requests
Use bypass() to mark requests that should skip MSW interception and hit the real server. This enables hybrid scenarios where some APIs are mocked and others are real.
Incorrect (request creates infinite loop):
// Making a fetch inside a handler without bypass creates infinite recursion
http.get('/api/user', async () => {
// This request is intercepted by this same handler!
const realResponse = await fetch('/api/user') // Infinite loop
return realResponse
})Correct (bypass MSW interception):
import { http, HttpResponse, bypass } from 'msw'
export const handlers = [
// Proxy to real API and modify response
http.get('/api/user', async ({ request }) => {
// bypass() marks the request to skip MSW
const realResponse = await fetch(bypass(request))
const realData = await realResponse.json()
// Augment real data with mock data
return HttpResponse.json({
...realData,
mockField: 'added by MSW',
})
}),
// Conditional passthrough
http.get('/api/data', async ({ request }) => {
const url = new URL(request.url)
// Only mock in test environment
if (url.searchParams.get('mock') !== 'true') {
return fetch(bypass(request))
}
return HttpResponse.json({ mocked: true })
}),
]Using passthrough() for unconditional passthrough:
import { http, passthrough } from 'msw'
export const handlers = [
// Always pass through to real API
http.get('/api/analytics/*', () => {
return passthrough()
}),
// Pass through external APIs
http.all('https://external-service.com/*', () => {
return passthrough()
}),
]Difference between bypass and passthrough:
bypass(request): Use inside a handler to make a real requestpassthrough(): Return from handler to let the original request through
When NOT to use this pattern:
- Fully mocked environments should avoid real API calls
Reference: MSW bypass API
Handle Cookies and Authentication
Access cookies from the cookies object in resolvers and set cookies via response headers. This enables testing of session-based authentication flows.
Incorrect (ignoring authentication state):
// Always returns user regardless of auth state
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
})Correct (cookie-based auth handling):
import { http, HttpResponse } from 'msw'
export const handlers = [
// Login sets session cookie
http.post('/api/login', async ({ request }) => {
const { email, password } = await request.json()
if (email === 'john@example.com' && password === 'password') {
return HttpResponse.json(
{ user: { id: '1', email } },
{
headers: {
'Set-Cookie': 'session=abc123; Path=/; HttpOnly; SameSite=Strict',
},
}
)
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}),
// Protected endpoint checks cookie
http.get('/api/user', ({ cookies }) => {
if (!cookies.session) {
return HttpResponse.json(
{ error: 'Not authenticated' },
{ status: 401 }
)
}
// In real app, validate session token
return HttpResponse.json({
id: '1',
name: 'John',
email: 'john@example.com',
})
}),
// Logout clears cookie
http.post('/api/logout', () => {
return new HttpResponse(null, {
status: 200,
headers: {
'Set-Cookie': 'session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
},
})
}),
]Bearer token authentication:
http.get('/api/protected', ({ request }) => {
const authHeader = request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return HttpResponse.json(
{ error: 'Missing token' },
{ status: 401 }
)
}
const token = authHeader.slice(7)
// Validate token (simplified)
if (token === 'valid-token') {
return HttpResponse.json({ data: 'protected content' })
}
return HttpResponse.json(
{ error: 'Invalid token' },
{ status: 403 }
)
})Multiple cookies:
http.get('/api/preferences', ({ cookies }) => {
return HttpResponse.json({
theme: cookies.theme || 'light',
language: cookies.lang || 'en',
userId: cookies.session ? '1' : null,
})
})When NOT to use this pattern:
- Public endpoints that don't require authentication
Reference: MSW Cookies
Implement Dynamic Mock Scenarios
Use module-level state or scenario flags to switch between different mock behaviors at runtime. This enables testing different application states without restarting tests.
Incorrect (static handlers for all scenarios):
// Can't test different user states without test setup changes
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John', plan: 'free' })
})Correct (dynamic scenario switching):
import { http, HttpResponse } from 'msw'
// Scenario state
type Scenario = 'default' | 'premium' | 'expired' | 'error'
let currentScenario: Scenario = 'default'
// Export function to change scenario
export function setScenario(scenario: Scenario) {
currentScenario = scenario
}
export const handlers = [
http.get('/api/user', () => {
switch (currentScenario) {
case 'premium':
return HttpResponse.json({
name: 'John',
plan: 'premium',
features: ['feature1', 'feature2', 'feature3'],
})
case 'expired':
return HttpResponse.json({
name: 'John',
plan: 'expired',
features: [],
message: 'Please renew your subscription',
})
case 'error':
return HttpResponse.json(
{ error: 'Service unavailable' },
{ status: 503 }
)
default:
return HttpResponse.json({
name: 'John',
plan: 'free',
features: ['feature1'],
})
}
}),
]Usage in tests:
import { setScenario } from '../mocks/handlers'
describe('Subscription', () => {
afterEach(() => {
setScenario('default') // Reset after each test
})
it('shows premium features for premium users', async () => {
setScenario('premium')
render(<Dashboard />)
expect(await screen.findByText('Premium Features')).toBeInTheDocument()
})
it('shows renewal prompt for expired users', async () => {
setScenario('expired')
render(<Dashboard />)
expect(await screen.findByText('Please renew')).toBeInTheDocument()
})
})URL-based scenarios (for development):
http.get('/api/user', ({ request }) => {
const url = new URL(request.url)
const scenario = url.searchParams.get('_scenario')
if (scenario === 'error') {
return new HttpResponse(null, { status: 500 })
}
return HttpResponse.json({ name: 'John' })
})
// In browser: /dashboard?_scenario=errorWhen NOT to use this pattern:
- Simple tests with single states don't need scenario complexity
Reference: Dynamic Mock Scenarios
Mock File Upload Endpoints
Parse FormData from requests to mock file upload endpoints. Access uploaded files and other form fields through the standard FormData API.
Incorrect (ignoring multipart data):
// Can't access uploaded file
http.post('/api/upload', () => {
return HttpResponse.json({ success: true })
})Correct (parsing FormData):
import { http, HttpResponse, delay } from 'msw'
export const handlers = [
// Single file upload
http.post('/api/upload', async ({ request }) => {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return HttpResponse.json(
{ error: 'No file provided' },
{ status: 400 }
)
}
// Simulate upload delay based on file size
await delay(Math.min(file.size / 1000, 2000))
return HttpResponse.json({
id: crypto.randomUUID(),
filename: file.name,
size: file.size,
mimeType: file.type,
url: `https://cdn.example.com/uploads/${file.name}`,
})
}),
// Multiple files
http.post('/api/upload-multiple', async ({ request }) => {
const formData = await request.formData()
const files = formData.getAll('files') as File[]
const results = files.map((file) => ({
id: crypto.randomUUID(),
filename: file.name,
size: file.size,
}))
return HttpResponse.json({ files: results })
}),
// File with additional form fields
http.post('/api/profile/avatar', async ({ request }) => {
const formData = await request.formData()
const avatar = formData.get('avatar') as File
const userId = formData.get('userId') as string
if (!avatar || !userId) {
return HttpResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
)
}
return HttpResponse.json({
userId,
avatarUrl: `https://cdn.example.com/avatars/${userId}/${avatar.name}`,
})
}),
]Validating file types:
http.post('/api/upload/image', async ({ request }) => {
const formData = await request.formData()
const file = formData.get('image') as File
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
return HttpResponse.json(
{ error: 'Invalid file type. Allowed: JPEG, PNG, GIF' },
{ status: 415 }
)
}
return HttpResponse.json({ success: true, url: '/uploaded.jpg' })
})When NOT to use this pattern:
- JSON-only APIs don't need FormData parsing
Reference: MSW Request FormData
Configure MSW for Vitest Browser Mode
Extend Vitest's test context to include the MSW worker when testing in browser mode. This ensures proper worker lifecycle management per test.
Incorrect (global worker without test context):
// Worker state leaks between tests
import { worker } from './mocks/browser'
beforeAll(() => worker.start())
afterAll(() => worker.stop())
// Tests can't override handlers safelyCorrect (extended test context):
// test-extend.ts
import { test as testBase } from 'vitest'
import { worker } from './mocks/browser'
export const test = testBase.extend({
worker: [
async ({}, use) => {
// Start worker before test
await worker.start({
onUnhandledRequest: 'error',
})
// Provide worker to test
await use(worker)
// Reset handlers after test
worker.resetHandlers()
},
{
auto: true, // Automatically available to all tests
},
],
})
export { expect } from 'vitest'// user.test.ts
import { http, HttpResponse } from 'msw'
import { test, expect } from './test-extend'
import { Dashboard } from './components/Dashboard'
test('displays user data', async ({ worker }) => {
// Worker is automatically started
render(<Dashboard />)
expect(await screen.findByText('John')).toBeInTheDocument()
})
test('handles error state', async ({ worker }) => {
// Override handlers for this test
worker.use(
http.get('/api/user', () => {
return new HttpResponse(null, { status: 500 })
})
)
render(<Dashboard />)
expect(await screen.findByText('Error loading')).toBeInTheDocument()
})Vitest config for browser mode:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
},
setupFiles: ['./vitest.setup.ts'],
},
})When NOT to use this pattern:
- Node.js tests should use
setupServerinstead
Reference: MSW Vitest Browser Mode
Know Common MSW Issues and Fixes
Reference this checklist when MSW behaves unexpectedly. Most issues fall into a few common categories that have well-known solutions.
Incorrect (handler not matching due to relative URL):
// Node.js environment - handler never matches
import { http, HttpResponse } from 'msw'
export const handlers = [
// Relative URL doesn't match absolute request URLs
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
]
// When fetch('http://localhost:3000/api/user') is called,
// the handler doesn't match because '/api/user' !== 'http://localhost:3000/api/user'Correct (handler matches with wildcard):
// Node.js environment - handler matches any origin
import { http, HttpResponse } from 'msw'
export const handlers = [
// Wildcard matches any origin prefix
http.get('*/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
]
// Now fetch('http://localhost:3000/api/user') matches correctlyCommon Issues Quick Reference
Issue: "fetch is not defined"
- Fix: Upgrade Node.js to 18+ (native fetch support)
Issue: Body parsing hangs with fake timers
// Fix: Exclude queueMicrotask from fake timers
vi.useFakeTimers({
toFake: ['setTimeout', 'setInterval', 'Date'],
// queueMicrotask NOT faked
})Issue: Stale responses from cache
// Fix: Clear request library cache between tests
afterEach(() => {
cache.clear() // SWR
queryClient.clear() // TanStack Query
})Issue: MSW v1 code in v2 project
// v2 correct syntax:
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
})When NOT to use this pattern:
- Reference only; not all issues apply to every project
Reference: MSW Debugging Runbook
Use Lifecycle Events for Debugging
Subscribe to lifecycle events to observe request interception, matching, and responses. This is invaluable for debugging why handlers aren't matching or responses aren't arriving.
Incorrect (guessing why requests fail):
// No visibility into what MSW is doing
const server = setupServer(...handlers)
server.listen()
// Tests fail mysteriously - is the handler matching? Is the response correct?Correct (lifecycle event logging):
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
const server = setupServer(...handlers)
// Log all intercepted requests
server.events.on('request:start', ({ request }) => {
console.log('MSW intercepted:', request.method, request.url)
})
// Log when a handler matches
server.events.on('request:match', ({ request }) => {
console.log('MSW matched:', request.method, request.url)
})
// Log completed responses
server.events.on('request:end', ({ request, response }) => {
console.log(
'MSW responded:',
request.method,
request.url,
'→',
response.status
)
})
// Log unhandled requests
server.events.on('request:unhandled', ({ request }) => {
console.warn('MSW unhandled:', request.method, request.url)
})Conditional logging:
// Only log in debug mode
if (process.env.DEBUG_MSW) {
server.events.on('request:start', ({ request }) => {
console.log('[MSW]', request.method, request.url)
})
}Event types:
request:start- Request intercepted by MSWrequest:match- Handler found for requestrequest:unhandled- No handler matchedrequest:end- Response sent (mocked or passthrough)response:mocked- Mocked response sentresponse:bypass- Request passed through to network
Cleanup:
// Remove event listeners when done
const unsubscribe = server.events.on('request:start', handler)
unsubscribe() // Remove listenerWhen NOT to use this pattern:
- Production builds should not include debug logging
- CI environments may want minimal logging for cleaner output
Reference: MSW Lifecycle Events
Log Request Details for Debugging
Create a debugging middleware handler to log full request details. This helps identify issues with request payloads, headers, and authentication.
Incorrect (no request visibility):
// Can't see what's being sent to handlers
http.post('/api/user', async ({ request }) => {
const body = await request.json()
// body is unexpected - but why?
return HttpResponse.json({ error: 'Invalid data' }, { status: 400 })
})Correct (detailed request logging):
import { http, HttpResponse } from 'msw'
// Debug middleware - add as first handler
const debugHandler = http.all('*', async ({ request }) => {
const url = new URL(request.url)
const clone = request.clone()
console.group(`[MSW] ${request.method} ${url.pathname}`)
console.log('Full URL:', request.url)
console.log('Headers:', Object.fromEntries(request.headers.entries()))
// Log body for non-GET requests
if (request.method !== 'GET' && request.method !== 'HEAD') {
const contentType = request.headers.get('Content-Type')
if (contentType?.includes('application/json')) {
console.log('Body (JSON):', await clone.json())
} else if (contentType?.includes('multipart/form-data')) {
const formData = await clone.formData()
console.log('Body (FormData):', Object.fromEntries(formData.entries()))
} else {
console.log('Body (Text):', await clone.text())
}
}
console.groupEnd()
// Don't return - let next handler process the request
})
export const handlers = [
debugHandler, // Must be first
// ... other handlers
]Conditional debug mode:
const DEBUG = process.env.DEBUG_MSW === 'true'
export const handlers = [
...(DEBUG ? [debugHandler] : []),
// ... other handlers
]Response logging:
// Log responses using lifecycle events
server.events.on('request:end', async ({ request, response }) => {
if (!response) return
const clone = response.clone()
const contentType = response.headers.get('Content-Type')
console.log(`[MSW Response] ${request.method} ${request.url}`)
console.log('Status:', response.status)
if (contentType?.includes('application/json')) {
console.log('Body:', await clone.json())
}
})Development browser logging:
// mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// Enable detailed logging in development
if (process.env.NODE_ENV === 'development') {
worker.start({
onUnhandledRequest: 'warn',
})
}When NOT to use this pattern:
- Remove debug logging before committing/production
- CI should use minimal logging for cleaner output
Reference: MSW Lifecycle Events
Verify Request Interception is Working
Add a verification step to confirm MSW is intercepting requests. This catches configuration issues before they cause mysterious test failures.
Incorrect (assuming MSW is working):
// No verification - tests fail with confusing errors
beforeAll(() => server.listen())
it('fetches user', async () => {
// If MSW isn't working, this hits real API or fails silently
const response = await fetch('/api/user')
})Correct (verify interception):
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
const server = setupServer(...handlers)
beforeAll(() => {
server.listen()
// Verify MSW is intercepting
server.events.on('request:start', ({ request }) => {
console.log('✓ MSW intercepted:', request.method, request.url)
})
})
// Or create a verification test
describe('MSW Setup', () => {
it('intercepts requests', async () => {
let intercepted = false
server.events.on('request:start', () => {
intercepted = true
})
await fetch('/api/health')
expect(intercepted).toBe(true)
})
})Debugging checklist when handlers don't match:
// Step 1: Verify interception is happening
server.events.on('request:start', ({ request }) => {
console.log('Intercepted URL:', request.url) // Check if URL is absolute
console.log('Intercepted method:', request.method)
})
// Step 2: Add console.log inside handler
http.get('/api/user', () => {
console.log('Handler matched!') // If this doesn't log, handler isn't matching
return HttpResponse.json({ name: 'John' })
})
// Step 3: Check handler URL vs request URL
// Common issues:
// - Handler: '/api/user' but request goes to 'http://localhost:3000/api/user'
// - Environment variable in URL is undefined
// - Typo in URL pathBrowser verification:
// In browser console
const { worker } = await import('./mocks/browser')
await worker.start()
// Check if worker script is accessible
fetch('/mockServiceWorker.js')
.then((r) => r.ok ? 'Worker script found' : 'Worker script missing')
.then(console.log)When NOT to use this pattern:
- Stable test suites don't need verification in every run
Reference: MSW Debugging Runbook
Handle Batched GraphQL Queries
Create a custom handler for batched GraphQL queries when using Apollo Client's batch link or similar. Batched requests send multiple operations in a single HTTP request.
Incorrect (individual handlers don't match batches):
// These won't match a batched request containing both queries
graphql.query('GetUser', () => {
return HttpResponse.json({ data: { user: { name: 'John' } } })
})
graphql.query('GetPosts', () => {
return HttpResponse.json({ data: { posts: [] } })
})
// Batched request is unhandled!Correct (batch handler with individual resolution):
import { http, HttpResponse, getResponse, bypass } from 'msw'
export function batchedGraphQLQuery(url: string, handlers: RequestHandler[]) {
return http.post(url, async ({ request }) => {
const requestClone = request.clone()
const payload = await request.json()
// Ignore non-batched requests
if (!Array.isArray(payload)) {
return
}
// Resolve each query in the batch
const responses = await Promise.all(
payload.map(async (operation) => {
const queryRequest = new Request(requestClone.url, {
method: 'POST',
headers: requestClone.headers,
body: JSON.stringify(operation),
})
const response = await getResponse(handlers, queryRequest)
return response || fetch(bypass(queryRequest))
})
)
// Combine responses into batch format
const results = await Promise.all(
responses.map((response) => response?.json())
)
return HttpResponse.json(results)
})
}// Usage
import { graphql, HttpResponse } from 'msw'
import { batchedGraphQLQuery } from './batchedGraphQLQuery'
const graphqlHandlers = [
graphql.query('GetUser', () => {
return HttpResponse.json({
data: { user: { id: '1', name: 'John' } },
})
}),
graphql.query('GetPosts', () => {
return HttpResponse.json({
data: { posts: [{ id: '1', title: 'Post 1' }] },
})
}),
]
export const handlers = [
batchedGraphQLQuery('/graphql', graphqlHandlers),
...graphqlHandlers, // Also handle non-batched requests
]When NOT to use this pattern:
- Apps not using query batching don't need batch handlers
Reference: MSW GraphQL Query Batching
Return GraphQL Errors in Correct Format
Return GraphQL errors in the standard errors array format. GraphQL errors are different from HTTP errors - they use status 200 with an errors field in the response body.
Incorrect (HTTP-style error response):
// GraphQL clients won't recognize this as a GraphQL error
graphql.query('GetUser', () => {
return HttpResponse.json(
{ error: 'User not found' },
{ status: 404 }
)
})Correct (GraphQL error format):
import { graphql, HttpResponse } from 'msw'
export const handlers = [
// Single error
graphql.query('GetUser', () => {
return HttpResponse.json({
data: null,
errors: [
{
message: 'User not found',
extensions: {
code: 'NOT_FOUND',
},
},
],
})
}),
// Validation errors with paths
graphql.mutation('CreateUser', () => {
return HttpResponse.json({
data: null,
errors: [
{
message: 'Invalid email format',
path: ['createUser', 'email'],
extensions: {
code: 'VALIDATION_ERROR',
field: 'email',
},
},
{
message: 'Name is required',
path: ['createUser', 'name'],
extensions: {
code: 'VALIDATION_ERROR',
field: 'name',
},
},
],
})
}),
// Partial success with errors
graphql.query('GetUsers', () => {
return HttpResponse.json({
data: {
users: [
{ id: '1', name: 'John' },
null, // This user failed to load
{ id: '3', name: 'Jane' },
],
},
errors: [
{
message: 'Failed to load user',
path: ['users', 1],
extensions: { code: 'INTERNAL_ERROR' },
},
],
})
}),
// Authentication error
graphql.query('GetPrivateData', () => {
return HttpResponse.json({
data: null,
errors: [
{
message: 'Not authenticated',
extensions: {
code: 'UNAUTHENTICATED',
},
},
],
})
}),
]Network-level errors (rare):
// For complete request failures, use HTTP error
graphql.query('GetUser', () => {
return HttpResponse.error() // Network failure
})When NOT to use this pattern:
- True network failures (DNS, connection) use
HttpResponse.error()
Reference: GraphQL Spec - Errors
Use Operation Name for GraphQL Matching
Match GraphQL operations by their operation name, not by URL. All GraphQL requests typically go to the same endpoint (/graphql), so operation name is the discriminator.
Incorrect (URL-based matching for GraphQL):
// All GraphQL requests go to /graphql - this catches everything!
http.post('/graphql', () => {
return HttpResponse.json({ data: { user: { name: 'John' } } })
})Correct (operation-based matching):
import { graphql, HttpResponse } from 'msw'
export const handlers = [
// Match query by operation name
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: {
id: '1',
name: 'John',
email: 'john@example.com',
},
},
})
}),
// Match mutation by operation name
graphql.mutation('CreateUser', async ({ variables }) => {
return HttpResponse.json({
data: {
createUser: {
id: crypto.randomUUID(),
name: variables.name,
email: variables.email,
},
},
})
}),
// Separate handlers for different queries
graphql.query('GetUsers', () => {
return HttpResponse.json({
data: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
},
})
}),
]Access operation variables:
graphql.query('GetUser', ({ variables }) => {
// variables matches your GraphQL query variables
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: 'John',
},
},
})
})
// Query from client:
// query GetUser($id: ID!) { user(id: $id) { id name } }
// variables: { id: "123" }Custom GraphQL endpoint:
// If your GraphQL endpoint isn't /graphql
graphql.link('https://api.example.com/gql').query('GetUser', () => {
return HttpResponse.json({
data: { user: { name: 'John' } },
})
})When NOT to use this pattern:
- REST APIs should use
http.*handlers, not GraphQL handlers
Reference: MSW GraphQL API
Access GraphQL Variables Correctly
Access GraphQL variables through the variables property in the resolver argument. Use them to return dynamic responses based on query input.
Incorrect (ignoring variables):
// Returns same user regardless of which ID is requested
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: { id: '1', name: 'John' },
},
})
})Correct (using variables for dynamic responses):
import { graphql, HttpResponse } from 'msw'
// Mock data store
const users = new Map([
['1', { id: '1', name: 'John', email: 'john@example.com' }],
['2', { id: '2', name: 'Jane', email: 'jane@example.com' }],
])
export const handlers = [
graphql.query('GetUser', ({ variables }) => {
const user = users.get(variables.id)
if (!user) {
return HttpResponse.json({
data: null,
errors: [{ message: 'User not found' }],
})
}
return HttpResponse.json({
data: { user },
})
}),
// Mutation with input variables
graphql.mutation('UpdateUser', ({ variables }) => {
const { id, input } = variables
const existingUser = users.get(id)
if (!existingUser) {
return HttpResponse.json({
data: null,
errors: [{ message: 'User not found' }],
})
}
const updatedUser = { ...existingUser, ...input }
users.set(id, updatedUser)
return HttpResponse.json({
data: { updateUser: updatedUser },
})
}),
// Pagination with variables
graphql.query('GetUsers', ({ variables }) => {
const { first = 10, after } = variables
const allUsers = Array.from(users.values())
let startIndex = 0
if (after) {
startIndex = allUsers.findIndex((u) => u.id === after) + 1
}
const pageUsers = allUsers.slice(startIndex, startIndex + first)
const hasNextPage = startIndex + first < allUsers.length
return HttpResponse.json({
data: {
users: {
edges: pageUsers.map((user) => ({
node: user,
cursor: user.id,
})),
pageInfo: {
hasNextPage,
endCursor: pageUsers[pageUsers.length - 1]?.id,
},
},
},
})
}),
]Type-safe variables:
type GetUserVariables = {
id: string
}
graphql.query<GetUserVariables>('GetUser', ({ variables }) => {
// variables.id is typed as string
return HttpResponse.json({
data: {
user: { id: variables.id, name: 'John' },
},
})
})When NOT to use this pattern:
- Queries without variables don't need variable access
Reference: MSW GraphQL Intercepting Operations
Use Absolute URLs in Handlers
Use absolute URLs or URL patterns in request handlers. Node.js environments require absolute URLs for proper matching, and relative URLs cause silent handler failures.
Incorrect (relative URLs in Node.js context):
// mocks/handlers.ts
export const handlers = [
// Relative URL - may not match in Node.js tests
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
]
// Test makes request to full URL
await fetch('http://localhost:3000/api/user')
// Handler doesn't match - request goes unhandledCorrect (absolute URLs or wildcards):
// mocks/handlers.ts
export const handlers = [
// Absolute URL - matches reliably
http.get('http://localhost:3000/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
]Better (wildcard for any origin):
// mocks/handlers.ts
export const handlers = [
// Wildcard matches any origin - most flexible
http.get('*/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
// Or use path patterns that MSW resolves against baseURL
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
]Environment variable pitfall:
// AVOID: Environment variable not set in tests
const API_URL = process.env.API_URL // undefined in tests!
http.get(`${API_URL}/user`, () => { /* ... */ })
// Results in: http.get('undefined/user', ...) - never matchesWhen NOT to use this pattern:
- Browser environments with properly configured base URLs may use relative paths
Reference: MSW Debugging Runbook
Group Handlers by Domain
Split handlers into separate files organized by domain or feature area. This prevents a single handlers file from becoming unmanageable and enables selective handler composition for different test scenarios.
Incorrect (monolithic handler file):
// mocks/handlers.ts - 500+ lines, unmaintainable
export const handlers = [
// User endpoints
http.get('/api/user', () => { /* ... */ }),
http.post('/api/user', () => { /* ... */ }),
http.delete('/api/user/:id', () => { /* ... */ }),
// Auth endpoints
http.post('/api/login', () => { /* ... */ }),
http.post('/api/logout', () => { /* ... */ }),
// Product endpoints
http.get('/api/products', () => { /* ... */ }),
// ... 50 more endpoints mixed together
]Correct (domain-organized handlers):
// mocks/handlers/user.ts
import { http, HttpResponse } from 'msw'
export const userHandlers = [
http.get('/api/user', () => {
return HttpResponse.json({ id: '1', name: 'John' })
}),
http.post('/api/user', async ({ request }) => {
const user = await request.json()
return HttpResponse.json(user, { status: 201 })
}),
http.delete('/api/user/:id', ({ params }) => {
return new HttpResponse(null, { status: 204 })
}),
]// mocks/handlers/auth.ts
import { http, HttpResponse } from 'msw'
export const authHandlers = [
http.post('/api/login', async ({ request }) => {
const { email, password } = await request.json()
return HttpResponse.json({ token: 'jwt-token' })
}),
http.post('/api/logout', () => {
return new HttpResponse(null, { status: 200 })
}),
]// mocks/handlers/index.ts - compose all handlers
import { userHandlers } from './user'
import { authHandlers } from './auth'
import { productHandlers } from './products'
export const handlers = [
...userHandlers,
...authHandlers,
...productHandlers,
]When NOT to use this pattern:
- Small projects with fewer than 10 endpoints may use a single file
Reference: Structuring Handlers - Group by Domain
Define Happy Path Handlers as Baseline
Define success-state handlers in a central handlers.ts file as your baseline. This establishes a reliable foundation that runtime overrides can modify for error scenarios, keeping test-specific edge cases separate from normal behavior.
Incorrect (mixing success and error states):
// mocks/handlers.ts - cluttered with all scenarios
export const handlers = [
http.get('/user', () => HttpResponse.json({ name: 'John' })),
http.get('/user', () => new HttpResponse(null, { status: 401 })),
http.get('/user', () => new HttpResponse(null, { status: 500 })),
http.get('/user', () => HttpResponse.error()),
// Confusing: which handler runs? Last one wins but intent unclear
]Correct (happy path baseline with runtime overrides):
// mocks/handlers.ts - clean success states only
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({ id: '1', name: 'John Maverick' })
}),
http.post('/api/login', async ({ request }) => {
const { email } = await request.json()
return HttpResponse.json({ token: 'mock-jwt-token', email })
}),
]// user.test.ts - override for specific scenarios
import { http, HttpResponse } from 'msw'
import { server } from '../mocks/node'
it('handles authentication error', () => {
server.use(
http.get('/api/user', () => {
return new HttpResponse(null, { status: 401 })
})
)
// Test error handling...
})When NOT to use this pattern:
- Single-use test utilities that will never need runtime overrides
Reference: Structuring Handlers
Explicitly Parse Request Bodies
MSW v2 does not automatically parse request bodies based on Content-Type. You must explicitly call .json(), .text(), .formData(), or .arrayBuffer() on the request object.
Incorrect (assuming auto-parsed body):
// MSW v1 pattern - body was auto-parsed
http.post('/api/user', ({ request }) => {
const body = request.body // undefined in v2!
return HttpResponse.json({ id: '1', ...body })
})Correct (explicit body parsing):
import { http, HttpResponse } from 'msw'
export const handlers = [
// JSON body
http.post('/api/user', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: '1', ...body }, { status: 201 })
}),
// Text body
http.post('/api/text', async ({ request }) => {
const text = await request.text()
return HttpResponse.text(`Received: ${text}`)
}),
// Form data
http.post('/api/upload', async ({ request }) => {
const formData = await request.formData()
const file = formData.get('file')
return HttpResponse.json({ filename: file?.name })
}),
// ArrayBuffer for binary
http.post('/api/binary', async ({ request }) => {
const buffer = await request.arrayBuffer()
return HttpResponse.json({ bytes: buffer.byteLength })
}),
]Typed body parsing:
interface CreateUserRequest {
name: string
email: string
}
http.post('/api/user', async ({ request }) => {
const body = await request.json() as CreateUserRequest
return HttpResponse.json({
id: crypto.randomUUID(),
name: body.name,
email: body.email,
})
})When NOT to use this pattern:
- GET requests typically don't have bodies and don't need parsing
Reference: MSW v2 Migration - Request Changes
Destructure Resolver Arguments Correctly
MSW v2 passes a single object argument to resolvers containing request, params, cookies, and other properties. Destructure from this object instead of using multiple arguments.
Incorrect (v1 multiple arguments pattern):
// MSW v1 pattern - does not work in v2
http.get('/api/user/:id', (req, res, ctx) => {
const userId = req.params.id // undefined!
return res(ctx.json({ id: userId }))
})Correct (v2 single object argument):
import { http, HttpResponse } from 'msw'
export const handlers = [
// Destructure from single argument object
http.get('/api/user/:id', ({ request, params, cookies }) => {
return HttpResponse.json({
id: params.id,
sessionId: cookies.sessionId,
userAgent: request.headers.get('User-Agent'),
})
}),
]Available resolver properties:
http.post('/api/data', async (info) => {
// Full request object
const { request } = info
// URL path parameters
const { params } = info // { id: '123' } for /api/data/:id
// Parsed cookies from Cookie header
const { cookies } = info // { session: 'abc', token: 'xyz' }
// Request URL as string (not URL object)
const url = new URL(request.url)
// Request method
const method = request.method
// Request headers
const authHeader = request.headers.get('Authorization')
// Request body (must await)
const body = await request.json()
return HttpResponse.json({ success: true })
})Type-safe params:
// Define params type for better type safety
type UserParams = {
id: string
orgId: string
}
http.get<UserParams>('/api/org/:orgId/user/:id', ({ params }) => {
// params.id and params.orgId are typed as string
return HttpResponse.json({ userId: params.id, orgId: params.orgId })
})When NOT to use this pattern:
- This is the only pattern in MSW v2; no alternatives
Reference: MSW v2 Migration - Resolver Changes
Share Handlers Across Environments
Define handlers once and reuse them across browser (development) and Node.js (testing) environments. This ensures mock behavior is consistent and prevents drift between development and test mocks.
Incorrect (duplicate handlers per environment):
// mocks/browser-handlers.ts - browser development
export const browserHandlers = [
http.get('/api/user', () => HttpResponse.json({ name: 'Dev User' })),
]
// mocks/test-handlers.ts - tests (different implementation!)
export const testHandlers = [
http.get('/api/user', () => HttpResponse.json({ name: 'Test User' })),
]
// Mock behavior differs between environments - bugs hide until productionCorrect (shared handlers, environment-specific setup):
// mocks/handlers.ts - single source of truth
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({ id: '1', name: 'John Maverick' })
}),
http.get('/api/posts', () => {
return HttpResponse.json([
{ id: '1', title: 'First Post' },
{ id: '2', title: 'Second Post' },
])
}),
]// mocks/browser.ts - browser setup
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)// mocks/node.ts - Node.js setup
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)// src/index.tsx - conditional browser activation
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') {
return
}
const { worker } = await import('./mocks/browser')
return worker.start()
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
})When NOT to use this pattern:
- Environment-specific handlers (e.g., browser-only features) can be added via separate arrays
Reference: MSW Comparison - Reusability
Extract Shared Response Logic into Resolvers
Extract repetitive response logic into reusable resolver functions. This ensures consistency across handlers and reduces maintenance when response shapes change.
Incorrect (duplicated response logic):
// mocks/handlers.ts
export const handlers = [
http.get('/api/user/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'John',
email: 'john@example.com',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
}),
http.get('/api/users', () => {
return HttpResponse.json([
{
id: '1',
name: 'John',
email: 'john@example.com',
createdAt: new Date().toISOString(), // Duplicated shape
updatedAt: new Date().toISOString(),
},
// ... more users with same structure
])
}),
]Correct (shared resolver factories):
// mocks/factories/user.ts
import { faker } from '@faker-js/faker'
export function createMockUser(overrides: Partial<User> = {}): User {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
...overrides,
}
}// mocks/handlers/user.ts
import { http, HttpResponse } from 'msw'
import { createMockUser } from '../factories/user'
export const userHandlers = [
http.get('/api/user/:id', ({ params }) => {
return HttpResponse.json(createMockUser({ id: params.id as string }))
}),
http.get('/api/users', () => {
return HttpResponse.json([
createMockUser(),
createMockUser(),
createMockUser(),
])
}),
]Higher-order handler for cross-cutting concerns:
// mocks/utils/withAuth.ts
import { HttpResponse } from 'msw'
import type { HttpResponseResolver } from 'msw'
export function withAuth(resolver: HttpResponseResolver): HttpResponseResolver {
return ({ request, ...rest }) => {
const authHeader = request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return new HttpResponse(null, { status: 401 })
}
return resolver({ request, ...rest })
}
}When NOT to use this pattern:
- One-off handlers for specific test scenarios don't need abstraction
Reference: Structuring Handlers
Use MSW v2 Response Syntax
MSW v2 replaces the composition-based res(ctx.json()) syntax with native Response and HttpResponse. Using v1 syntax in v2 causes handlers to fail silently.
Incorrect (v1 composition syntax):
// MSW v1 syntax - does not work in v2
import { rest } from 'msw'
export const handlers = [
rest.get('/api/user', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({ name: 'John' }),
ctx.delay(100)
)
}),
]Correct (v2 HttpResponse syntax):
// MSW v2 syntax
import { http, HttpResponse, delay } from 'msw'
export const handlers = [
http.get('/api/user', async () => {
await delay(100)
return HttpResponse.json(
{ name: 'John' },
{ status: 200 }
)
}),
]Common v2 response patterns:
import { http, HttpResponse, delay } from 'msw'
export const handlers = [
// JSON response
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
// Text response
http.get('/api/text', () => {
return HttpResponse.text('Hello, World!')
}),
// XML response
http.get('/api/xml', () => {
return HttpResponse.xml('<user><name>John</name></user>')
}),
// Empty response with status
http.delete('/api/user/:id', () => {
return new HttpResponse(null, { status: 204 })
}),
// Custom headers
http.get('/api/data', () => {
return HttpResponse.json(
{ data: 'value' },
{
headers: {
'X-Custom-Header': 'custom-value',
},
}
)
}),
// Network error
http.get('/api/error', () => {
return HttpResponse.error()
}),
]When NOT to use this pattern:
- Projects still on MSW v1 should use the v1 syntax until migration
Reference: MSW v2 Migration Guide
Use Custom Predicates for Complex Matching
Use a predicate function instead of a URL string for complex matching logic. This enables matching based on headers, request body, cookies, or any combination of request properties.
Incorrect (multiple handlers for same URL):
// Awkward - two handlers for same URL with different behavior
http.get('/api/data', () => {
return HttpResponse.json({ public: true })
})
// How to handle authenticated vs unauthenticated?Correct (predicate-based matching):
import { http, HttpResponse } from 'msw'
export const handlers = [
// Match based on header presence
http.get('/api/data', ({ request }) => {
const authHeader = request.headers.get('Authorization')
if (authHeader?.startsWith('Bearer ')) {
return HttpResponse.json({ data: 'authenticated content' })
}
return HttpResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}),
]Predicate function syntax:
// Match by custom criteria
http.all(
({ request }) => {
// Return true to match this request
return request.url.includes('/api/') &&
request.headers.get('X-Custom-Header') === 'special'
},
() => {
return HttpResponse.json({ matched: true })
}
)Match by request body content:
http.post(
'/api/action',
async ({ request }) => {
const body = await request.clone().json()
// Different response based on action type
if (body.action === 'create') {
return HttpResponse.json({ id: '1', created: true })
}
if (body.action === 'delete') {
return new HttpResponse(null, { status: 204 })
}
return HttpResponse.json(
{ error: 'Unknown action' },
{ status: 400 }
)
}
)Match by cookie value:
http.get('/api/user', ({ cookies }) => {
if (cookies.role === 'admin') {
return HttpResponse.json({ permissions: ['read', 'write', 'delete'] })
}
return HttpResponse.json({ permissions: ['read'] })
})When NOT to use this pattern:
- Simple URL-based matching is clearer when sufficient
Reference: MSW Custom Request Predicate
Order Handlers from Specific to General
Place more specific handlers before general ones in the handlers array. MSW matches handlers in order, and the first match wins. A wildcard handler placed first will shadow all subsequent handlers.
Incorrect (general handler shadows specific):
export const handlers = [
// Wildcard matches first - specific handlers never reached!
http.get('/api/*', () => {
return HttpResponse.json({ fallback: true })
}),
// Never matches because /api/* already caught it
http.get('/api/user/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'John' })
}),
]Correct (specific before general):
import { http, HttpResponse } from 'msw'
export const handlers = [
// Most specific handlers first
http.get('/api/user/:id/settings', ({ params }) => {
return HttpResponse.json({ userId: params.id, theme: 'dark' })
}),
http.get('/api/user/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'John' })
}),
http.get('/api/users', () => {
return HttpResponse.json([{ id: '1' }, { id: '2' }])
}),
// General fallback last
http.get('/api/*', () => {
return HttpResponse.json(
{ error: 'Not found' },
{ status: 404 }
)
}),
]Middleware with passthrough:
export const handlers = [
// Middleware runs first but doesn't return - passes through
http.all('*', async () => {
await delay(50) // Add delay to all requests
// No return = continue to next handler
}),
// Specific handlers still match
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
]Runtime handler precedence:
// Initial handlers
const server = setupServer(
http.get('/api/user', () => HttpResponse.json({ name: 'John' }))
)
// Runtime handlers are PREPENDED (take precedence)
server.use(
http.get('/api/user', () => HttpResponse.json({ name: 'Jane' }))
)
// Request to /api/user returns { name: 'Jane' }When NOT to use this pattern:
- Single handlers don't have ordering concerns
Reference: MSW Handler Precedence
Match HTTP Methods Explicitly
Use method-specific handlers (http.get, http.post, etc.) instead of http.all. This prevents a GET handler from accidentally matching POST requests and ensures correct REST API simulation.
Incorrect (catching unintended methods):
// Matches ALL methods - GET, POST, PUT, DELETE, etc.
http.all('/api/user', () => {
return HttpResponse.json({ name: 'John' })
})
// POST /api/user also returns user JSON instead of creatingCorrect (method-specific handlers):
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET - retrieve resource
http.get('/api/user/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'John' })
}),
// POST - create resource
http.post('/api/user', async ({ request }) => {
const body = await request.json()
return HttpResponse.json(
{ id: crypto.randomUUID(), ...body },
{ status: 201 }
)
}),
// PUT - replace resource
http.put('/api/user/:id', async ({ params, request }) => {
const body = await request.json()
return HttpResponse.json({ id: params.id, ...body })
}),
// PATCH - partial update
http.patch('/api/user/:id', async ({ params, request }) => {
const updates = await request.json()
return HttpResponse.json({ id: params.id, name: 'John', ...updates })
}),
// DELETE - remove resource
http.delete('/api/user/:id', () => {
return new HttpResponse(null, { status: 204 })
}),
// HEAD - metadata only
http.head('/api/user/:id', () => {
return new HttpResponse(null, {
headers: { 'X-User-Exists': 'true' },
})
}),
// OPTIONS - CORS preflight
http.options('/api/*', () => {
return new HttpResponse(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
},
})
}),
]When to use http.all:
// Global middleware (logging, delay)
http.all('*', async () => {
await delay(50)
// No return - continues to next handler
})
// Catch-all fallback for testing
http.all('/api/*', () => {
console.warn('Unhandled API request')
return new HttpResponse(null, { status: 404 })
})When NOT to use this pattern:
- Middleware handlers that should apply to all methods can use
http.all
Reference: MSW HTTP Handlers
Access Query Parameters from Request URL
Parse query parameters from request.url using the URL constructor. MSW does not automatically parse query strings, so you must extract them manually.
Incorrect (assuming params includes query string):
http.get('/api/users', ({ params }) => {
// params does NOT contain query parameters!
const page = params.page // undefined
return HttpResponse.json([])
})Correct (parse from request.url):
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/users', ({ request }) => {
const url = new URL(request.url)
const page = url.searchParams.get('page') || '1'
const limit = url.searchParams.get('limit') || '10'
const search = url.searchParams.get('search')
// Mock paginated response
const pageNum = parseInt(page, 10)
const limitNum = parseInt(limit, 10)
return HttpResponse.json({
data: mockUsers.slice((pageNum - 1) * limitNum, pageNum * limitNum),
meta: {
page: pageNum,
limit: limitNum,
total: mockUsers.length,
},
})
}),
// Filtering by query params
http.get('/api/products', ({ request }) => {
const url = new URL(request.url)
const category = url.searchParams.get('category')
const minPrice = url.searchParams.get('minPrice')
const maxPrice = url.searchParams.get('maxPrice')
let products = [...mockProducts]
if (category) {
products = products.filter((p) => p.category === category)
}
if (minPrice) {
products = products.filter((p) => p.price >= Number(minPrice))
}
if (maxPrice) {
products = products.filter((p) => p.price <= Number(maxPrice))
}
return HttpResponse.json(products)
}),
]Multiple values for same parameter:
http.get('/api/items', ({ request }) => {
const url = new URL(request.url)
// /api/items?tag=red&tag=blue&tag=green
const tags = url.searchParams.getAll('tag') // ['red', 'blue', 'green']
return HttpResponse.json(
mockItems.filter((item) => tags.some((tag) => item.tags.includes(tag)))
)
})When NOT to use this pattern:
- Endpoints that don't use query parameters
Reference: MSW Request Object
Use URL Path Parameters Correctly
Use :paramName syntax for dynamic path segments. Parameters are available in the resolver's params object. Incorrect patterns cause handlers to never match.
Incorrect (literal URL instead of pattern):
// Only matches exact string "/api/user/123"
http.get('/api/user/123', () => {
return HttpResponse.json({ name: 'John' })
})
// Request to /api/user/456 is unhandledCorrect (parameterized URL):
import { http, HttpResponse } from 'msw'
export const handlers = [
// Matches /api/user/123, /api/user/abc, etc.
http.get('/api/user/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'John' })
}),
// Multiple parameters
http.get('/api/org/:orgId/user/:userId', ({ params }) => {
return HttpResponse.json({
orgId: params.orgId,
userId: params.userId,
})
}),
// Optional parameters (use separate handlers)
http.get('/api/posts', () => {
return HttpResponse.json([{ id: '1' }, { id: '2' }])
}),
http.get('/api/posts/:postId', ({ params }) => {
return HttpResponse.json({ id: params.postId })
}),
]Wildcard patterns:
// Match any path starting with /api/
http.get('/api/*', () => {
return HttpResponse.json({ fallback: true })
})
// Match any origin with specific path
http.get('*/api/user', () => {
return HttpResponse.json({ name: 'John' })
})
// Match all requests of a method
http.get('*', () => {
return HttpResponse.json({ catchAll: true })
})Type-safe parameters:
type UserParams = {
userId: string
}
http.get<UserParams>('/api/user/:userId', ({ params }) => {
// params.userId is typed as string
return HttpResponse.json({ id: params.userId })
})When NOT to use this pattern:
- Exact URL matching for specific endpoints that never vary
Reference: MSW Request Matching
Set Response Headers Correctly
Set appropriate response headers to simulate real API behavior. Headers like Set-Cookie, Cache-Control, and CORS headers affect how your application handles responses.
Incorrect (missing important headers):
http.post('/api/login', () => {
// Missing Set-Cookie header - authentication won't work
return HttpResponse.json({ user: { id: '1' } })
})
http.get('/api/data', () => {
// Missing caching headers - can't test cache behavior
return HttpResponse.json({ data: 'value' })
})Correct (explicit headers):
import { http, HttpResponse } from 'msw'
export const handlers = [
// Authentication with cookies
http.post('/api/login', () => {
return HttpResponse.json(
{ user: { id: '1', name: 'John' } },
{
headers: {
'Set-Cookie': 'session=abc123; Path=/; HttpOnly',
},
}
)
}),
// Caching headers
http.get('/api/static-data', () => {
return HttpResponse.json(
{ version: '1.0' },
{
headers: {
'Cache-Control': 'public, max-age=3600',
'ETag': '"abc123"',
},
}
)
}),
// No-cache for dynamic data
http.get('/api/user', () => {
return HttpResponse.json(
{ name: 'John' },
{
headers: {
'Cache-Control': 'no-store',
},
}
)
}),
// Pagination headers
http.get('/api/users', () => {
return HttpResponse.json(
[{ id: '1' }, { id: '2' }],
{
headers: {
'X-Total-Count': '100',
'X-Page': '1',
'X-Per-Page': '10',
'Link': '</api/users?page=2>; rel="next"',
},
}
)
}),
// Rate limiting headers
http.get('/api/limited', () => {
return HttpResponse.json(
{ data: 'value' },
{
headers: {
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': '99',
'X-RateLimit-Reset': String(Date.now() + 3600000),
},
}
)
}),
]Testing rate limit handling:
it('shows rate limit warning when approaching limit', async () => {
server.use(
http.get('/api/data', () => {
return HttpResponse.json(
{ data: 'value' },
{
headers: {
'X-RateLimit-Remaining': '5',
},
}
)
})
)
render(<DataFetcher />)
expect(await screen.findByText('Rate limit warning')).toBeInTheDocument()
})When NOT to use this pattern:
- Tests not concerned with header-specific behavior can omit custom headers
Reference: MSW HttpResponse Options
Add Realistic Response Delays
Use delay() to simulate network latency in handlers. This reveals race conditions, tests loading states, and ensures your application handles realistic response timing.
Incorrect (instant responses hide timing issues):
http.get('/api/user', () => {
// Instant response - loading states never visible
// Race conditions in component never triggered
return HttpResponse.json({ name: 'John' })
})// Component test passes but has hidden race condition
it('displays user', async () => {
render(<UserProfile />)
// Loading state flashes so briefly it's never testable
expect(await screen.findByText('John')).toBeInTheDocument()
})Correct (realistic delays):
import { http, HttpResponse, delay } from 'msw'
export const handlers = [
http.get('/api/user', async () => {
// Simulate typical API latency
await delay(150)
return HttpResponse.json({ name: 'John' })
}),
]Testing loading states:
it('shows loading indicator while fetching', async () => {
server.use(
http.get('/api/user', async () => {
await delay(500) // Longer delay for visibility
return HttpResponse.json({ name: 'John' })
})
)
render(<UserProfile />)
// Loading state is visible during delay
expect(screen.getByText('Loading...')).toBeInTheDocument()
// Content appears after delay
expect(await screen.findByText('John')).toBeInTheDocument()
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
})Global delay for all handlers:
import { http, delay } from 'msw'
export const handlers = [
// Apply delay to all requests
http.all('*', async () => {
await delay(100)
// No return = continue to next matching handler
}),
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John' })
}),
]Delay modes:
// Fixed delay
await delay(200)
// Random delay within range (simulates variable network)
await delay('real') // Random 100-400ms
// Infinite delay (simulates hung request)
await delay('infinite')When NOT to use this pattern:
- Unit tests focused on business logic may skip delays for speed
- CI pipelines may use shorter delays to reduce test duration
Reference: MSW delay() API
Simulate Error Responses Correctly
Use proper HTTP status codes and HttpResponse.error() for network errors. Different error types require different handling in your application, and correct simulation ensures your error handling works.
Incorrect (ambiguous error responses):
// Unclear what type of error this represents
http.get('/api/user', () => {
return HttpResponse.json({ error: 'Something went wrong' })
})
// Missing status code - defaults to 200!
http.get('/api/user', () => {
return new HttpResponse('Error occurred')
})Correct (explicit error types):
import { http, HttpResponse } from 'msw'
// HTTP 4xx - Client errors
http.get('/api/user/:id', () => {
return HttpResponse.json(
{ error: 'User not found' },
{ status: 404 }
)
})
http.post('/api/user', () => {
return HttpResponse.json(
{ error: 'Validation failed', fields: ['email'] },
{ status: 400 }
)
})
http.get('/api/protected', () => {
return HttpResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
})
http.get('/api/admin', () => {
return HttpResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
)
})
// HTTP 5xx - Server errors
http.get('/api/user', () => {
return HttpResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
})
http.get('/api/data', () => {
return new HttpResponse(null, { status: 503 }) // Service unavailable
})
// Network error (connection failure, DNS failure, etc.)
http.get('/api/user', () => {
return HttpResponse.error() // Causes fetch to reject
})Testing error handling:
it('displays 404 message when user not found', async () => {
server.use(
http.get('/api/user/:id', () => {
return HttpResponse.json(
{ error: 'User not found' },
{ status: 404 }
)
})
)
render(<UserProfile userId="999" />)
expect(await screen.findByText('User not found')).toBeInTheDocument()
})
it('handles network failure gracefully', async () => {
server.use(
http.get('/api/user', () => {
return HttpResponse.error()
})
)
render(<UserProfile />)
expect(await screen.findByText('Network error')).toBeInTheDocument()
})When NOT to use this pattern:
- Happy path tests should use success responses
Reference: MSW HttpResponse API
Use HttpResponse Static Methods
Use HttpResponse.json(), HttpResponse.text(), and other static methods instead of manually constructing responses. These methods automatically set correct Content-Type headers and provide cleaner, more readable code.
Incorrect (manual response construction):
http.get('/api/user', () => {
// Verbose, easy to forget Content-Type header
return new Response(
JSON.stringify({ name: 'John' }),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
}
)
})Correct (HttpResponse static methods):
import { http, HttpResponse } from 'msw'
export const handlers = [
// JSON - sets Content-Type: application/json automatically
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John', email: 'john@example.com' })
}),
// JSON with status code
http.post('/api/user', () => {
return HttpResponse.json(
{ id: '1', message: 'Created' },
{ status: 201 }
)
}),
// Text - sets Content-Type: text/plain
http.get('/api/text', () => {
return HttpResponse.text('Hello, World!')
}),
// XML - sets Content-Type: application/xml
http.get('/api/xml', () => {
return HttpResponse.xml('<user><name>John</name></user>')
}),
// HTML - sets Content-Type: text/html
http.get('/page', () => {
return HttpResponse.html('<html><body><h1>Hello</h1></body></html>')
}),
// ArrayBuffer for binary data
http.get('/api/file', () => {
const buffer = new ArrayBuffer(8)
return HttpResponse.arrayBuffer(buffer, {
headers: { 'Content-Type': 'application/octet-stream' },
})
}),
// FormData
http.get('/api/form', () => {
const form = new FormData()
form.append('field', 'value')
return HttpResponse.formData(form)
}),
]When NOT to use this pattern:
- Custom Content-Types not covered by helpers may need manual
new Response()
Reference: MSW HttpResponse API
Use One-Time Handlers for Sequential Scenarios
Use the { once: true } option for handlers that should only respond once, then fall back to the next matching handler. This is essential for testing retry logic, sequential API calls, or state changes between requests.
Incorrect (permanent override prevents testing retries):
it('retries after failure', async () => {
server.use(
http.get('/api/user', () => {
return new HttpResponse(null, { status: 500 })
})
)
render(<UserProfile />)
// Component retries, but always gets 500 - can't test success after retry
await userEvent.click(screen.getByRole('button', { name: 'Retry' }))
// Still failing...
})Correct (one-time handler for first request):
import { http, HttpResponse } from 'msw'
it('retries after failure and succeeds', async () => {
server.use(
http.get(
'/api/user',
() => {
return new HttpResponse(null, { status: 500 })
},
{ once: true } // Only affects first request
)
)
render(<UserProfile />)
// First request fails
expect(await screen.findByText('Error loading user')).toBeInTheDocument()
// Retry succeeds (uses baseline happy-path handler)
await userEvent.click(screen.getByRole('button', { name: 'Retry' }))
expect(await screen.findByText('John')).toBeInTheDocument()
})Sequential state changes:
it('shows optimistic update then server response', async () => {
let callCount = 0
server.use(
http.post('/api/like', () => {
callCount++
if (callCount === 1) {
// First call - slow response simulates network
return HttpResponse.json({ likes: 11 })
}
// Subsequent calls
return HttpResponse.json({ likes: 12 })
})
)
// Test optimistic update behavior
})Multiple sequential states:
server.use(
// First request: pending
http.get('/api/order/:id', () => {
return HttpResponse.json({ status: 'pending' })
}, { once: true }),
// Second request: processing
http.get('/api/order/:id', () => {
return HttpResponse.json({ status: 'processing' })
}, { once: true }),
// Third+ requests: completed
http.get('/api/order/:id', () => {
return HttpResponse.json({ status: 'completed' })
})
)When NOT to use this pattern:
- Single-state tests that don't involve retries or polling
Reference: Network Behavior Overrides
Mock Streaming Responses with ReadableStream
Use ReadableStream to mock streaming responses for Server-Sent Events (SSE), chunked transfers, or streaming APIs. This enables testing of real-time features and progressive UI updates.
Incorrect (returning complete response immediately):
// Chat response arrives all at once - doesn't test streaming UI
http.post('/api/chat', () => {
return HttpResponse.json({
message: 'Here is the complete response all at once',
})
})Correct (streaming response):
import { http, HttpResponse } from 'msw'
// Text streaming (like AI chat)
http.post('/api/chat', () => {
const encoder = new TextEncoder()
const chunks = ['Hello', ' ', 'world', '!']
const stream = new ReadableStream({
async start(controller) {
for (const chunk of chunks) {
controller.enqueue(encoder.encode(chunk))
await new Promise((resolve) => setTimeout(resolve, 100))
}
controller.close()
},
})
return new HttpResponse(stream, {
headers: {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked',
},
})
})Server-Sent Events (SSE):
http.get('/api/events', () => {
const encoder = new TextEncoder()
const events = [
{ id: '1', data: { message: 'First event' } },
{ id: '2', data: { message: 'Second event' } },
{ id: '3', data: { message: 'Third event' } },
]
const stream = new ReadableStream({
async start(controller) {
for (const event of events) {
const sseMessage = `id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`
controller.enqueue(encoder.encode(sseMessage))
await new Promise((resolve) => setTimeout(resolve, 200))
}
controller.close()
},
})
return new HttpResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
})JSON streaming (newline-delimited):
http.get('/api/stream-json', () => {
const encoder = new TextEncoder()
const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
]
const stream = new ReadableStream({
async start(controller) {
for (const item of items) {
controller.enqueue(encoder.encode(JSON.stringify(item) + '\n'))
await new Promise((resolve) => setTimeout(resolve, 100))
}
controller.close()
},
})
return new HttpResponse(stream, {
headers: { 'Content-Type': 'application/x-ndjson' },
})
})When NOT to use this pattern:
- APIs that return complete responses don't need streaming mocks
Reference: MSW ReadableStream Support
Configure Server Lifecycle in Test Setup
Server lifecycle hooks must be configured in test setup files to ensure proper initialization, cleanup between tests, and graceful shutdown. Missing hooks cause handler pollution and unpredictable test behavior.
Incorrect (no lifecycle management):
// handlers.test.ts
import { server } from './mocks/node'
// Server never started, handlers never reset
// Tests may pass/fail randomly depending on execution order
it('fetches user', async () => {
const response = await fetch('/user')
// This fails silently - no mocking active
})Correct (proper lifecycle hooks):
// vitest.setup.ts or setupTests.ts
import { beforeAll, afterEach, afterAll } from 'vitest'
import { server } from './mocks/node'
// Start server before all tests
beforeAll(() => server.listen())
// Reset handlers after each test to ensure isolation
afterEach(() => server.resetHandlers())
// Clean shutdown after all tests complete
afterAll(() => server.close())Jest equivalent:
// setupTests.ts
import { server } from './mocks/node'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())When NOT to use this pattern:
- Browser-based tests use
worker.start()andworker.stop()instead
Reference: MSW Quick Start - Test Setup
Require Node.js 18+ for MSW v2
MSW v2 sets Node.js 18.0.0 as the minimum supported version. Older versions lack native fetch and other required APIs, causing complete mocking failure.
Incorrect (using unsupported Node version):
// package.json
{
"engines": {
"node": ">=14.0.0"
}
}# Node 16 - MSW v2 fails silently or throws cryptic errors
$ node -v
v16.20.0
$ npm test
# ReferenceError: fetch is not defined
# or: Cannot read properties of undefinedCorrect (enforce Node 18+ requirement):
// package.json
{
"engines": {
"node": ">=18.0.0"
}
}# .nvmrc
18# CI configuration (GitHub Actions)
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]Verify installation:
# Check Node version
node -v # Should be v18.x.x or higher
# Check MSW version
npm ls msw # Should be v2.x.xWhen NOT to use this pattern:
- If stuck on Node 16, use MSW v1.x instead (with different API patterns)
Reference: MSW v2 Migration - Requirements
Use Correct Entrypoint for Node.js
MSW v2 separates browser and Node.js entrypoints. Using the wrong import causes complete mocking failure with no interception occurring.
Incorrect (importing from wrong path):
// This imports browser code into Node.js - mocking silently fails
import { setupServer } from 'msw'
import { setupWorker } from 'msw'
const server = setupServer(...handlers)Correct (using msw/node entrypoint):
// Node.js environments (tests, SSR) use msw/node
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)Browser environments use msw/browser:
// Browser environments use msw/browser
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)When NOT to use this pattern:
- Never deviate from this pattern; entrypoint selection is binary based on environment
Reference: MSW Node.js Integration
Configure TypeScript for MSW v2
MSW v2 requires TypeScript 4.7+ for proper type inference. Incorrect TypeScript configuration causes confusing type errors and prevents proper handler typing.
Incorrect (outdated TypeScript or missing config):
// tsconfig.json - missing moduleResolution
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext"
// No moduleResolution - imports fail to resolve
}
}// Type errors due to incorrect resolution
import { http, HttpResponse } from 'msw'
// Error: Cannot find module 'msw' or its corresponding type declarationsCorrect (proper TypeScript configuration):
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler", // or "node16" / "nodenext"
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}Typed handlers with proper inference:
import { http, HttpResponse } from 'msw'
interface User {
id: string
name: string
email: string
}
export const handlers = [
http.get<never, never, User>('/api/user/:id', ({ params }) => {
// params.id is typed as string
return HttpResponse.json({
id: params.id,
name: 'John Doe',
email: 'john@example.com',
})
}),
]When NOT to use this pattern:
- JavaScript-only projects don't need TypeScript configuration
- Projects using older bundlers may need
moduleResolution: "node"
Reference: MSW v2 Migration - TypeScript
Configure Unhandled Request Behavior
Configure onUnhandledRequest: 'error' to fail tests when requests lack handlers. This catches missing mocks immediately instead of allowing silent network calls or undefined behavior.
Incorrect (default silent behavior):
// mocks/node.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
// Default behavior: warns but doesn't fail
// Unhandled requests may hit real APIs or return undefined
export const server = setupServer(...handlers)// Test passes despite missing handler - false positive!
it('submits form', async () => {
await submitForm({ email: 'test@example.com' })
// POST /submit has no handler but test doesn't fail
expect(screen.getByText('Success')).toBeInTheDocument()
})Correct (strict unhandled request handling):
// vitest.setup.ts
import { server } from './mocks/node'
beforeAll(() => {
server.listen({
onUnhandledRequest: 'error', // Fail on any unhandled request
})
})
afterEach(() => server.resetHandlers())
afterAll(() => server.close())Custom handling for specific URLs:
beforeAll(() => {
server.listen({
onUnhandledRequest(request, print) {
// Allow certain requests through (e.g., static assets)
if (request.url.includes('/static/')) {
return
}
// Error on all other unhandled requests
print.error()
},
})
})When NOT to use this pattern:
- Development environments may prefer
'warn'for less disruptive feedback - Integration tests that intentionally hit real endpoints
Commit Worker Script to Version Control
The mockServiceWorker.js file generated by msw init should be committed to Git. This ensures team members and CI environments can run browser tests without additional setup commands.
Incorrect (gitignoring worker script):
# .gitignore
# This forces every developer to run msw init manually
public/mockServiceWorker.js# New team member clones repo, runs tests, gets cryptic errors
# because worker script doesn't exist
npm test
# Error: Cannot find mockServiceWorker.jsCorrect (commit and auto-update worker script):
# Generate worker script with --save flag
npx msw init public/ --save// package.json - auto-updates worker on msw version changes
{
"msw": {
"workerDirectory": ["public"]
}
}# .gitignore - do NOT ignore the worker script
# public/mockServiceWorker.js <- remove this lineMonorepo configuration:
// Root package.json for monorepos
{
"msw": {
"workerDirectory": [
"apps/web/public",
"apps/admin/public"
]
}
}When NOT to use this pattern:
- If treating as generated artifact, ensure CI runs
msw initbefore tests
Reference: Managing the Worker
Use Async Testing Utilities for Mock Responses
Use async testing utilities (findBy, waitFor) instead of getBy or manual setTimeout when testing components that make API calls. Mock responses are asynchronous, and synchronous assertions will fail before data arrives.
Incorrect (synchronous assertions):
it('displays user name', () => {
render(<UserProfile />)
// Fails! Response hasn't arrived yet
expect(screen.getByText('John')).toBeInTheDocument()
})
// Also incorrect - arbitrary timeout
it('displays user name', async () => {
render(<UserProfile />)
await new Promise((resolve) => setTimeout(resolve, 100))
// Flaky - 100ms might not be enough, or wastes time if faster
expect(screen.getByText('John')).toBeInTheDocument()
})Correct (async testing utilities):
import { render, screen, waitFor } from '@testing-library/react'
it('displays user name', async () => {
render(<UserProfile />)
// findBy* returns a promise that resolves when element appears
expect(await screen.findByText('John')).toBeInTheDocument()
})
// For non-element assertions, use waitFor
it('updates document title', async () => {
render(<UserProfile />)
await waitFor(() => {
expect(document.title).toBe('John - Profile')
})
})
// For multiple elements
it('displays user list', async () => {
render(<UserList />)
const users = await screen.findAllByRole('listitem')
expect(users).toHaveLength(3)
})With loading states:
it('shows loading then content', async () => {
render(<UserProfile />)
// Loading state appears immediately
expect(screen.getByText('Loading...')).toBeInTheDocument()
// Content replaces loading after response
expect(await screen.findByText('John')).toBeInTheDocument()
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
})When NOT to use this pattern:
- Synchronous operations that don't involve API calls can use
getBy
Reference: Testing Library Async Methods
Avoid Direct Request Assertions
Do not assert that specific requests were made with specific parameters. This tests implementation details rather than behavior. Instead, assert on your application's reaction to responses.
Incorrect (asserting request details):
it('sends correct data when creating user', async () => {
const requestSpy = vi.fn()
server.use(
http.post('/api/user', async ({ request }) => {
requestSpy(await request.json())
return HttpResponse.json({ id: '1' })
})
)
await createUser({ name: 'John', email: 'john@example.com' })
// Testing implementation details - what if we add a field?
expect(requestSpy).toHaveBeenCalledWith({
name: 'John',
email: 'john@example.com',
})
})Correct (assert behavior through responses):
it('creates user and shows success message', async () => {
server.use(
http.post('/api/user', async ({ request }) => {
const body = await request.json()
// Validate in handler - returns error if invalid
if (!body.email) {
return HttpResponse.json(
{ error: 'Email required' },
{ status: 400 }
)
}
return HttpResponse.json({ id: '1', ...body }, { status: 201 })
})
)
render(<CreateUserForm />)
await userEvent.type(screen.getByLabelText('Name'), 'John')
await userEvent.type(screen.getByLabelText('Email'), 'john@example.com')
await userEvent.click(screen.getByRole('button', { name: 'Create' }))
// Assert application behavior, not request details
expect(await screen.findByText('User created!')).toBeInTheDocument()
})For one-way requests (analytics, logging):
import { server } from './mocks/node'
it('tracks page view', async () => {
const trackingPromise = new Promise<void>((resolve) => {
server.events.on('request:end', ({ request }) => {
if (request.url.includes('/analytics')) {
resolve()
}
})
})
render(<HomePage />)
// Verify the request was made without asserting payload
await expect(trackingPromise).resolves.toBeUndefined()
})When NOT to use this pattern:
- Analytics/telemetry testing may require request payload verification via lifecycle events
Reference: Avoid Request Assertions
Clear Request Library Caches Between Tests
Clear data fetching library caches between tests. Libraries like SWR, TanStack Query, and Apollo cache responses, causing tests to receive stale data from previous tests instead of fresh mocked responses.
Incorrect (cached data leaks between tests):
// test-a.spec.ts
it('displays original user', async () => {
// Response cached by SWR/React Query
render(<UserProfile />)
expect(await screen.findByText('John')).toBeInTheDocument()
})
// test-b.spec.ts
it('displays updated user', async () => {
server.use(
http.get('/api/user', () => {
return HttpResponse.json({ name: 'Jane' }) // Different user
})
)
render(<UserProfile />)
// FAILS! Cache returns 'John' from previous test
expect(await screen.findByText('Jane')).toBeInTheDocument()
})Correct (clear caches in test setup):
// SWR cache clearing
import { cache } from 'swr'
afterEach(() => {
// Clear SWR cache
cache.clear()
})// TanStack Query cache clearing
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
},
})
const renderWithClient = (ui: React.ReactElement) => {
const testQueryClient = createTestQueryClient()
return render(
<QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
)
}
// Each test gets fresh QueryClient with no cached data
it('displays user', async () => {
renderWithClient(<UserProfile />)
expect(await screen.findByText('John')).toBeInTheDocument()
})// Apollo Client cache clearing
import { ApolloClient, InMemoryCache } from '@apollo/client'
afterEach(async () => {
await client.clearStore()
// or client.resetStore() to refetch active queries
})When NOT to use this pattern:
- Tests intentionally verifying cache behavior should not clear caches
Reference: MSW Debugging Runbook
Use server.boundary() for Concurrent Tests
Wrap concurrent tests in server.boundary() to isolate handler overrides. Without boundaries, concurrent tests share handlers, causing race conditions and unpredictable failures.
Incorrect (concurrent tests without boundaries):
// Tests run in parallel - handlers leak between them
it.concurrent('fetches user', async () => {
// Uses initial handlers
const response = await fetch('https://api.example.com/user')
expect(response.ok).toBe(true)
})
it.concurrent('handles error', async () => {
server.use(
http.get('https://api.example.com/user', () => {
return new HttpResponse(null, { status: 500 })
})
)
// This override might affect the other concurrent test!
const response = await fetch('https://api.example.com/user')
expect(response.status).toBe(500)
})Correct (boundary-isolated concurrent tests):
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
http.get('https://api.example.com/user', () => {
return HttpResponse.json({ name: 'John' })
})
)
beforeAll(() => server.listen())
afterAll(() => server.close())
it.concurrent(
'fetches user',
server.boundary(async () => {
const response = await fetch('https://api.example.com/user')
const user = await response.json()
expect(user).toEqual({ name: 'John' })
})
)
it.concurrent(
'handles server error',
server.boundary(async () => {
// Override is scoped to this boundary only
server.use(
http.get('https://api.example.com/user', () => {
return new HttpResponse(null, { status: 500 })
})
)
const response = await fetch('https://api.example.com/user')
expect(response.status).toBe(500)
})
)
it.concurrent(
'handles network error',
server.boundary(async () => {
server.use(
http.get('https://api.example.com/user', () => {
return HttpResponse.error()
})
)
await expect(fetch('https://api.example.com/user')).rejects.toThrow()
})
)When NOT to use this pattern:
- Sequential tests (non-concurrent) can use standard
afterEachreset pattern
Reference: server.boundary() API
Configure Fake Timers to Preserve queueMicrotask
When using fake timers, configure them to not mock queueMicrotask. The global fetch uses queueMicrotask internally for body parsing, and mocking it causes request.json() and similar methods to hang forever.
Incorrect (default fake timers):
// vitest.config.ts or jest.config.js
beforeEach(() => {
vi.useFakeTimers() // Mocks ALL timer functions including queueMicrotask
})
// Test hangs indefinitely
it('parses request body', async () => {
server.use(
http.post('/api/user', async ({ request }) => {
const body = await request.json() // Never resolves!
return HttpResponse.json(body)
})
)
await fetch('/api/user', {
method: 'POST',
body: JSON.stringify({ name: 'John' }),
})
})Correct (exclude queueMicrotask from fake timers):
// Vitest
beforeEach(() => {
vi.useFakeTimers({
toFake: [
'setTimeout',
'setInterval',
'clearTimeout',
'clearInterval',
'setImmediate',
'clearImmediate',
'Date',
],
// queueMicrotask is NOT in the list - remains real
})
})
// Or more explicitly with shouldAdvanceTime
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true })
})// Jest
beforeEach(() => {
jest.useFakeTimers({
doNotFake: ['queueMicrotask'], // Explicitly preserve queueMicrotask
})
})Alternative - advance timers after async operations:
it('handles delayed response', async () => {
vi.useFakeTimers()
const fetchPromise = fetch('/api/user')
// Advance timers to allow microtasks to process
await vi.runAllTimersAsync()
const response = await fetchPromise
expect(response.ok).toBe(true)
vi.useRealTimers()
})When NOT to use this pattern:
- If not using fake timers, this configuration is unnecessary
Reference: MSW Debugging Runbook
Use Correct JSDOM Environment for Jest
Use jest-fixed-jsdom instead of jest-environment-jsdom when testing with MSW in Jest. Standard JSDOM uses browser export conditions but runs in Node.js, causing conflicts with MSW's entrypoints.
Incorrect (standard jsdom environment):
// jest.config.js
module.exports = {
testEnvironment: 'jsdom', // Uses browser exports incorrectly
}// Tests fail with cryptic errors
// TypeError: Cannot read properties of undefined
// or: fetch is not definedCorrect (use jest-fixed-jsdom):
npm install -D jest-fixed-jsdom// jest.config.js
module.exports = {
testEnvironment: 'jest-fixed-jsdom',
}Alternative - migrate to Vitest:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'jsdom', // Vitest handles this correctly
setupFiles: ['./vitest.setup.ts'],
},
})Why this matters:
JSDOM uses browser export conditions by default, causing packages like MSW to use browser-specific code in a Node.js context. This leads to:
- Missing Node.js globals
- Incorrect module resolution
- Silent failures or cryptic errors
jest-fixed-jsdom and Vitest correctly handle the Node.js/browser boundary.
When NOT to use this pattern:
- Vitest users don't need this; Vitest handles environments correctly
- Node-only tests (no DOM) should use
testEnvironment: 'node'
Reference: MSW v2 Migration - Jest/JSDOM
Reset Handlers After Each Test
Call server.resetHandlers() in afterEach to remove runtime handlers added during tests. Without reset, handlers from one test can affect subsequent tests, causing mysterious failures that depend on test execution order.
Incorrect (no handler reset):
// test-a.spec.ts
it('handles server error', () => {
server.use(
http.get('/api/user', () => new HttpResponse(null, { status: 500 }))
)
// Test passes...
})
// test-b.spec.ts - runs after test-a
it('displays user name', () => {
// FAILS! Still receiving 500 error from test-a's handler
render(<UserProfile />)
expect(screen.getByText('John')).toBeInTheDocument()
})Correct (reset in afterEach):
// vitest.setup.ts
import { server } from './mocks/node'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers()) // Clean slate for each test
afterAll(() => server.close())// user.test.ts
it('handles server error', () => {
server.use(
http.get('/api/user', () => new HttpResponse(null, { status: 500 }))
)
render(<UserProfile />)
expect(screen.getByRole('alert')).toHaveTextContent('Error')
})
// Handler is removed after this test completes
it('displays user name', () => {
// Uses baseline happy-path handler
render(<UserProfile />)
expect(screen.getByText('John')).toBeInTheDocument()
})When NOT to use this pattern:
server.boundary()provides automatic isolation for concurrent tests
Reference: Network Behavior Overrides
Related skills
How it compares
Choose msw over ad-hoc fetch stubs when you need shared browser and Node interceptors with explicit handler architecture rules.
FAQ
Which MSW version does the msw skill target?
The msw skill targets Mock Service Worker v2 from mswjs and is labeled version 1.0.0, with rules written for current handler APIs rather than legacy v1 patterns.
How many guidelines does the msw skill include?
The msw skill bundles 45+ rules organized into 8 categories, ranked from critical setup and handler architecture down to incremental debugging practices.
Is Msw safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.