Mocking API Responses with MSW (Mock Service Worker) in React & Vue
MSW (Mock Service Worker) intercepts network requests at the browser or Node level, so your app calls fetch() or axios exactly as it would in production — the mock lives underneath the network layer instead of inside your components. That makes it the most realistic way to mock APIs for both development and automated tests, and it works identically in React, Vue, and any other framework.
Why MSW Instead of Just Faking Data in a Component?
- Your components never know they're talking to a mock — no conditional "if mock mode" branches in application code.
- The same handlers work in the browser (dev mode) and in Node (Jest/Vitest), so you write the mock once.
- You can simulate real HTTP behavior: status codes, delays, headers, and even flaky failures.
Step 1: Install and Define Handlers
npm install msw --save-devGenerate the fake payload first — with a schema that matches your real API — using Dummy JSON Generator, then reference it inside your handler:
// mocks/handlers.ts
import { http, HttpResponse, delay } from 'msw'
import products from '../mock/products.json'
export const handlers = [
http.get('/api/products', async () => {
await delay(200)
return HttpResponse.json(products)
}),
http.get('/api/products/:id', ({ params }) => {
const product = products.find(p => p.id === Number(params.id))
return product
? HttpResponse.json(product)
: new HttpResponse(null, { status: 404 })
}),
]Step 2: Start the Worker in the Browser (React or Vue — Same Code)
npx msw init public/ --save// mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)Start it once, conditionally, in your app entry point — main.ts in Vue, index.tsx in React — using the exact same pattern:
if (globalThis._importMeta_.env.DEV && globalThis._importMeta_.env.VITE_USE_MOCKS === 'true') {
const { worker } = await import('./mocks/browser')
await worker.start()
}Step 3: Use the Same Handlers in Tests
This is where MSW earns its keep — the handlers you wrote for local dev work unchanged inside Jest or Vitest.
// mocks/server.ts (Node, for tests)
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)// setupTests.ts
import { server } from './mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())Step 4: Override a Handler for One Test (Testing Error States)
The real value of MSW is testing what happens when things go wrong — without touching a real backend.
import { http, HttpResponse } from 'msw'
import { server } from '../mocks/server'
test('shows an error message when the API fails', async () => {
server.use(
http.get('/api/products', () => new HttpResponse(null, { status: 500 }))
)
// render component, assert error UI appears
})Common Pitfalls
- Forgetting
resetHandlers()between tests — leftover overrides leak into the next test and cause confusing failures. - Mocking the wrong layer — MSW intercepts network requests, not your data-fetching library's internals; if you're mocking
axiositself instead, you lose the "app doesn't know it's mocked" benefit. - Committing the service worker script without regenerating it after upgrading MSW — always re-run
msw initafter a version bump.
The Bottom Line
MSW is framework-agnostic by design, so the setup above is identical whether you're in React or Vue. Generate realistic fixture data first, wire it into handlers once, and reuse those same handlers for local development, Storybook, and your test suite.