MSW intercepts network requests at the service worker level — http.get("/api/users", () => HttpResponse.json(users)) mocks a GET endpoint. http.post("/api/auth/login", async ({ request }) => { const body = await request.json(); return HttpResponse.json({ token: "..." }) }) mocks POST with body parsing. setupWorker(...handlers) creates the browser service worker. worker.start({ onUnhandledRequest: "warn" }) starts it. setupServer(...handlers) creates a Node.js interceptor for tests. server.use(http.get("/api/users", () => HttpResponse.json([]))) overrides a handler at runtime. server.resetHandlers() removes overrides. server.close() tears down after tests. HttpResponse.error() simulates network failures. delay(200) adds latency. http.all("/api/*", passthrough) proxies real requests. graphql.query("GetUser", ...) mocks GraphQL. once handlers fire once then fall through. Claude Code generates MSW mocks, Vitest setup, and storybook integration.
CLAUDE.md for MSW
## MSW Stack
- Version: msw >= 2.7
- Handlers: http.get, http.post, http.put, http.patch, http.delete — return HttpResponse.json(data, { status: 200 })
- Browser: const worker = setupWorker(...handlers); await worker.start({ onUnhandledRequest: "warn" })
- Node (Vitest/Jest): const server = setupServer(...handlers); beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterEach(() => server.resetHandlers()); afterAll(() => server.close())
- Override in test: server.use(http.get("/api/users", () => HttpResponse.json([])))
- Error: return HttpResponse.error() — simulates network failure
- Delay: import { delay } from "msw"; await delay(300)
Request Handlers
// mocks/handlers/auth.ts — authentication handlers
import { http, HttpResponse, delay } from "msw"
import { z } from "zod"
type User = { id: string; name: string; email: string; role: string; avatar?: string }
// Shared in-memory state for the mock server
const USERS: User[] = [
{ id: "user-1", name: "Alice Johnson", email: "[email protected]", role: "admin", avatar: undefined },
{ id: "user-2", name: "Bob Smith", email: "[email protected]", role: "user" },
]
const sessions = new Map<string, User>()
export const authHandlers = [
http.post("/api/auth/login", async ({ request }) => {
await delay(50) // Realistic latency
const body = await request.json() as { email: string; password: string }
const user = USERS.find((u) => u.email === body.email)
if (!user || body.password !== "password") {
return HttpResponse.json(
{ error: "Invalid credentials" },
{ status: 401 },
)
}
const token = `mock-jwt-${user.id}-${Date.now()}`
sessions.set(token, user)
return HttpResponse.json({
token,
user: { id: user.id, name: user.name, email: user.email, role: user.role },
})
}),
http.post("/api/auth/logout", ({ request }) => {
const token = request.headers.get("Authorization")?.replace("Bearer ", "")
if (token) sessions.delete(token)
return HttpResponse.json({ ok: true })
}),
http.get("/api/auth/me", ({ request }) => {
const token = request.headers.get("Authorization")?.replace("Bearer ", "")
const user = token ? sessions.get(token) : null
if (!user) {
return HttpResponse.json({ error: "Unauthorized" }, { status: 401 })
}
return HttpResponse.json(user)
}),
]
// mocks/handlers/posts.ts — posts CRUD handlers
import { http, HttpResponse, delay } from "msw"
type Post = { id: string; title: string; body: string; authorId: string; published: boolean; createdAt: string }
const posts: Post[] = Array.from({ length: 20 }, (_, i) => ({
id: `post-${i + 1}`,
title: `Sample Post ${i + 1}`,
body: `This is the content of post ${i + 1}. It contains enough text to be meaningful.`,
authorId: i % 2 === 0 ? "user-1" : "user-2",
published: i < 15,
createdAt: new Date(Date.now() - i * 86400000).toISOString(),
}))
export const postHandlers = [
http.get("/api/posts", ({ request }) => {
const url = new URL(request.url)
const page = parseInt(url.searchParams.get("page") ?? "1")
const limit = parseInt(url.searchParams.get("limit") ?? "10")
const published = url.searchParams.get("published")
let filtered = published === "true" ? posts.filter((p) => p.published) : posts
const total = filtered.length
const data = filtered.slice((page - 1) * limit, page * limit)
return HttpResponse.json({ posts: data, total, page, totalPages: Math.ceil(total / limit) })
}),
http.get("/api/posts/:id", ({ params }) => {
const post = posts.find((p) => p.id === params.id)
if (!post) return HttpResponse.json({ error: "Not found" }, { status: 404 })
return HttpResponse.json(post)
}),
http.post("/api/posts", async ({ request }) => {
await delay(80)
const body = await request.json() as Omit<Post, "id" | "createdAt">
const newPost: Post = {
id: `post-${posts.length + 1}`,
createdAt: new Date().toISOString(),
...body,
}
posts.unshift(newPost)
return HttpResponse.json(newPost, { status: 201 })
}),
http.patch("/api/posts/:id", async ({ params, request }) => {
await delay(60)
const index = posts.findIndex((p) => p.id === params.id)
if (index === -1) return HttpResponse.json({ error: "Not found" }, { status: 404 })
const update = await request.json() as Partial<Post>
posts[index] = { ...posts[index], ...update }
return HttpResponse.json(posts[index])
}),
http.delete("/api/posts/:id", ({ params }) => {
const index = posts.findIndex((p) => p.id === params.id)
if (index === -1) return HttpResponse.json({ error: "Not found" }, { status: 404 })
posts.splice(index, 1)
return new HttpResponse(null, { status: 204 })
}),
]
Server Setup for Vitest
// mocks/server.ts — MSW server setup for Node.js tests
import { setupServer } from "msw/node"
import { authHandlers } from "./handlers/auth"
import { postHandlers } from "./handlers/posts"
export const server = setupServer(
...authHandlers,
...postHandlers,
)
// vitest.setup.ts — global test setup
import "@testing-library/jest-dom"
import { server } from "./mocks/server"
import { afterAll, afterEach, beforeAll } from "vitest"
// Start server before all tests
beforeAll(() => {
server.listen({ onUnhandledRequest: "error" })
})
// Reset handler overrides between tests
afterEach(() => {
server.resetHandlers()
})
// Shutdown after all tests
afterAll(() => {
server.close()
})
Component Test with MSW Override
// components/PostList.test.tsx — test with MSW overrides
import { describe, it, expect } from "vitest"
import { render, screen, waitFor } from "@testing-library/react"
import { http, HttpResponse } from "msw"
import { server } from "@/mocks/server"
import { PostList } from "./PostList"
describe("PostList", () => {
it("renders posts from API", async () => {
render(<PostList />)
await waitFor(() => {
expect(screen.getByText("Sample Post 1")).toBeInTheDocument()
})
})
it("shows empty state when no posts", async () => {
// Override just for this test
server.use(
http.get("/api/posts", () =>
HttpResponse.json({ posts: [], total: 0, page: 1, totalPages: 0 }),
),
)
render(<PostList />)
await waitFor(() => {
expect(screen.getByText(/no posts/i)).toBeInTheDocument()
})
})
it("shows error state on network failure", async () => {
server.use(
http.get("/api/posts", () => HttpResponse.error()),
)
render(<PostList />)
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument()
})
})
it("shows loading skeleton during fetch", async () => {
server.use(
http.get("/api/posts", async () => {
const { delay } = await import("msw")
await delay(200)
return HttpResponse.json({ posts: [], total: 0, page: 1, totalPages: 0 })
}),
)
render(<PostList />)
expect(screen.getByTestId("loading-skeleton")).toBeInTheDocument()
})
})
For the Vitest/Jest vi.mock alternative when mocking specific module imports (functions, classes, utilities) rather than HTTP network requests — module mocks intercept import calls while MSW intercepts at the network layer, making MSW far more realistic since the actual fetch/axios code runs unchanged, see the Vitest Advanced guide. For the Nock (Node.js) alternative when intercepting HTTP in Node.js-only environments without service worker support, recording/replaying real HTTP, or mocking non-standard protocols — Nock is HTTP-library-specific while MSW works with any fetch-based code and runs in both browser and Node, see the testing guide. The Claude Skills 360 bundle includes MSW skill sets covering handler libraries, test setup, and Storybook integration. Start with the free tier to try API mocking generation.