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.
Table of Contents
- 1. Mocking at the network level instead of the function level
- 2. Setup for Vitest
- 3. Request handlers for typical REST endpoints
- 4. The advantage over vi.mock() with Axios/Fetch
- 5. A component test working together with MSW
- 6. Browser setup vs. Node setup
- 7. Deliberately simulating error paths and network failures
- 8. Keeping a reusable handler structure
- 9. Limits of MSW and what it doesn't replace
- 10. Summary
- 11. FAQ
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