Mock Service Worker (MSW) for API Mocking in Vue Tests
AI generated
{ }
Vue · Testing · MSW
Mock Service Worker for Vue
API mocking at the network level instead of the function level

Mock Service Worker intercepts HTTP requests at the network boundary before they're even sent, instead of replacing individual JavaScript functions like Axios calls. That makes Vue tests more realistic, since the full request-response cycle runs, including status codes, headers, and error paths, without the test code needing to know which HTTP library a component uses internally.

14 min read Mock Service Worker Vitest

1. Mocking at the network level instead of the function level

Classic mocking with vi.mock('axios') or vi.mock('./api') replaces an entire module with a fake implementation defined by the test code itself. That works, but it tightly couples the test to the concrete implementation: if a component switches from Axios to Fetch, or the internal structure of the API module changes, every mock has to be updated, even though the application's actual behavior hasn't changed at all.

Mock Service Worker (MSW) takes a different approach: it intercepts requests directly at the network level, either through a real service worker in the browser or through Node interception in test environments, and answers them with defined handlers. The component under test doesn't notice a thing; it sends a completely normal HTTP request through Fetch or Axios and gets back a response that looks like a real server response. The test stays independent of which HTTP library is used internally.


// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/products/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Teapot',
      price: 29.9,
    })
  }),

  http.post('/api/cart/items', async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json({ id: 'cart-item-1', ...body }, { status: 201 })
  }),

  http.get('/api/products/:id/reviews', () => {
    return HttpResponse.json([], { status: 200 })
  }),
]

2. Setup for Vitest

Setting up MSW for Vitest goes through setupServer from msw/node, which registers the handlers in a test setup file. Vitest loads this setup file automatically before every test run, so every test file gets access to the same, centrally defined handlers by default without having to build its own mocking. The server starts before all tests, resets after every single test, and closes after all tests finish.

It's important not to forget the server reset after each test, since handler overrides from one test can otherwise leak into the next. A clean reset ensures every test starts from the same, predictable baseline regardless of the order test files run in. This is a detail that's easy to overlook, especially in parallel test suites, but causes hard-to-debug, sporadically failing tests when missing.

3. Request handlers for typical REST endpoints

Handlers in MSW are defined per HTTP method and path, with path parameters like :id automatically parsed and made available through the handler's params object. For typical CRUD endpoints, one handler per method and resource is usually enough: GET for single fetch and list retrieval, POST for creation, PUT or PATCH for updates, DELETE for deletion. Query parameters can be read from the request object's url property, for example for filtering or pagination logic.

For error paths, the same endpoint can be overridden for a single test without permanently changing the global handler. Using server.use(...) inside a specific test simulates an error response for that one test case, such as a 500 status code or a network error response, while every other test keeps using the successful default handler. That makes testing error boundaries and UI error messages considerably easier than with manually constructed mock return values.

4. The advantage over vi.mock() with Axios/Fetch

With vi.mock('axios'), the test code has to know exactly which Axios method is used internally for every call under test, whether it's axios.get, an Axios instance created with axios.create(), or an interceptor involved somewhere along the way. If that internal structure changes, for instance because a team moves from direct Axios calls to a central API client class, every vi.mock call in every affected test needs updating, even though the application's HTTP contract hasn't changed at all.

MSW abstracts away exactly these implementation details, because it operates at the network boundary rather than the JavaScript API boundary. Switching from Axios to Fetch, or introducing a custom API wrapper module, requires no changes to the MSW handlers as long as the actual HTTP requests stay the same. That makes tests more resilient to internal refactoring and significantly reduces coupling between test code and implementation details, which brings a noticeable maintenance benefit especially in larger Vue projects with many API calls.

5. A component test working together with MSW

In a concrete component test, MSW usually runs in the background through the global setup, without the individual test file importing anything directly, except when a handler needs to be overridden for that specific test. The component under test loads data as usual through its normal API layer, MSW answers the request behind the scenes, and the test then checks whether the component correctly displays the received data.

This approach makes it possible to realistically test genuine async flows, including loading states between request and response, since MSW only delivers the response after a real (though fast) promise cycle. Tests relying on await flushPromises() or similar helpers to wait for the asynchronous response end up behaving closer to the application's actual runtime behavior than they would with synchronously resolved, manually constructed mocks.

6. Browser setup vs. Node setup

MSW distinguishes between two operating modes: in the browser, for example for Storybook or a local development environment, a real service worker registers through setupWorker and intercepts requests before they leave the browser. In Node-based test runners like Vitest, setupServer from msw/node is used instead, which relies on an interception library since Node has no concept of a real service worker.

The key advantage is that the same handler definitions can be reused across both environments. A handler file written for Storybook, to develop components in isolation with realistic API responses, plugs into Vitest tests unchanged. That saves duplicated maintenance of mock data and ensures the development environment and the test environment share the same assumptions about the API shape.

7. Deliberately simulating error paths and network failures

Beyond normal success responses, MSW also lets you test behavior under genuine network failures, for example through HttpResponse.error(), which simulates a complete connection failure, as opposed to a response with an error status code like 404 or 500. This distinction matters because a lot of frontend error handling reacts differently to a rejected fetch promise than it does to a technically successful response carrying an error status.

Delays can be simulated deliberately too, for example through the delay() helper from the MSW package, to realistically test loading states and race conditions between multiple concurrent requests. A test that checks whether a loading indicator correctly disappears once the last of several parallel requests has been answered benefits noticeably from a controlled but realistic delay, instead of relying on the somewhat arbitrary timing characteristics of synchronous mocks.

8. Keeping a reusable handler structure

In larger projects it pays off to organize handlers by domain, for instance one file for product endpoints, one for cart endpoints, and one for account endpoints, instead of collecting every handler in a single file that quickly becomes hard to navigate. These handler files can then be imported selectively, so a test only loads the handlers actually relevant to that particular test area, which makes the test file more readable and reduces unintended interactions between unrelated test areas.

Another benefit of this structure shows up when onboarding new team members: reading the handler files also gives a documented overview of the API endpoints actually in use and their expected response shape, which is often more current than a separate API documentation, since the handlers have to be maintained alongside the tests to keep them passing.

9. Limits of MSW and what it doesn't replace

As realistically as MSW simulates the request-response cycle, it never checks whether the handler definitions actually match the real backend API. A handler can happily return a field the real API renamed long ago without any test noticing, since MSW simply returns whatever the handler defines, regardless of whether that shape still matches reality. This gap between the mock and the real API is known as contract drift and is a known risk with any form of mocking, not just MSW.

To limit that risk, many teams complement their MSW handlers with regular contract tests against the real API, for example in a separate test suite that actually runs against a staging environment and spot-checks whether the assumed response shape still holds. MSW therefore doesn't replace integration tests against a real backend; it complements them with a fast, isolated layer for frontend logic, while responsibility for the correctness of the handler definitions themselves still rests with the developers maintaining them.

Aspect vi.mock() (function level) MSW (network level)
Coupling tied to a specific HTTP library independent of Axios/Fetch
Realism manually constructed return values real request-response cycle
Reuse redefined per test file central handler files
Error paths must be simulated manually HttpResponse.error() and status codes
Storybook compatibility not directly usable same handlers reusable

Mironsoft

Vue architecture, Composition API, and Nuxt performance

Vue applications that don't get more complicated with every feature?

We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.

Architecture Review

Checking composables, state management, and component structure for maintainability.

Performance Audit

Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.

Nuxt Integration

Building robust, type-safe SSR/SSG setup and API integration.

10. Summary

MSW for Vue tests: key takeaways at a glance

Approach

MSW mocks at the network level, not the function level

Setup

setupServer from msw/node, don't forget to reset after every test

Handlers

defined per HTTP method and path, overridable per test

Benefit

independent of whether Axios, Fetch, or a custom API wrapper is used

11. FAQ: MSW for Vue tests: key takeaways at a glance

1What's the core difference between MSW and vi.mock()?
vi.mock() replaces a JavaScript module with a fake implementation, while MSW intercepts the actual HTTP request at the network level. That keeps the test code independent of which HTTP library is used internally.
2Do I need to reset MSW after every test?
Yes, through server.resetHandlers() in an afterEach hook. Without a reset, handler overrides from one test can leak into subsequent tests and cause sporadic failures.
3Can I use the same handlers in Storybook and Vitest?
Yes, that's one of the main advantages. The same handler definitions work with setupWorker in the browser and with setupServer in Node test runners.
4How do I simulate a network error with MSW?
Through HttpResponse.error() inside a handler. That simulates a complete connection failure, as opposed to a response carrying an error status code like 500.
5Does MSW work with Fetch instead of Axios?
Yes, MSW is independent of the HTTP library used, since it operates at the network level. Both Fetch and Axios calls get intercepted the same way.
6How do I override a handler for just a single test?
Through server.use(...) inside that specific test. The override only applies until the next reset and doesn't affect any other tests.
7Do I need a real service worker for Vitest?
No, in Node environments like Vitest, setupServer from msw/node is used, which relies on an interception library instead of a real browser service worker.
8Can I test loading states with MSW?
Yes, the delay() function from the MSW package lets you add a controlled delay to the response, to realistically test loading indicators and race conditions.
9How should I organize handlers in a larger project?
Best split by domain across several files, for example product, cart, and account handlers, instead of collecting everything in a single file.
10Is MSW worth it for small projects with few API calls?
For a handful of stable endpoints, vi.mock() might be enough. Once several components share the same endpoints or the HTTP library might change, the decoupling MSW provides usually pays off early.