You're probably mocking APIs wrong. Custom mock servers, intercepting fetch globally, hardcoding responses — all fragile, all pain.
MSW (Mock Service Worker) fixes this at the network level.
What is MSW?
MSW intercepts HTTP requests at the network level using Service Workers (browser) or custom interceptors (Node.js). Your application code doesn't know it's being mocked. That's the point.
Why MSW Changes Everything
1. Network-Level Interception
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
}),
http.post('/api/users', async ({ request }) => {
const user = await request.json();
return HttpResponse.json({ id: 3, ...user }, { status: 201 });
}),
];
Your fetch(), axios, ky — all intercepted. No code changes needed.
2. Same Mocks for Browser AND Node.js
// Browser (Storybook, dev server)
import { setupWorker } from 'msw/browser';
const worker = setupWorker(...handlers);
worker.start();
// Node.js (tests, SSR)
import { setupServer } from 'msw/node';
const server = setupServer(...handlers);
server.listen();
Write mocks once. Use everywhere.
3. Request Handlers That Feel Natural
import { http, HttpResponse } from 'msw';
const handlers = [
// Path parameters
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'Alice' });
}),
// Query parameters
http.get('/api/search', ({ request }) => {
const url = new URL(request.url);
const query = url.searchParams.get('q');
return HttpResponse.json({ results: [`Result for ${query}`] });
}),
// Error responses
http.get('/api/protected', () => {
return HttpResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}),
// Network errors
http.get('/api/unstable', () => {
return HttpResponse.error();
}),
];
4. Perfect for Testing
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('shows user profile', async () => {
// Override for this specific test
server.use(
http.get('/api/users/1', () => {
return HttpResponse.json({ id: 1, name: 'Test User', role: 'admin' });
})
);
render(<UserProfile userId="1" />);
expect(await screen.findByText('Test User')).toBeInTheDocument();
});
test('handles API errors gracefully', async () => {
server.use(
http.get('/api/users/1', () => {
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
})
);
render(<UserProfile userId="1" />);
expect(await screen.findByText('User not found')).toBeInTheDocument();
});
5. WebSocket Mocking
import { ws } from 'msw';
const chat = ws.link('wss://api.example.com/chat');
export const handlers = [
chat.addEventListener('connection', ({ client }) => {
client.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
client.addEventListener('message', (event) => {
client.send(JSON.stringify({
type: 'echo',
data: event.data
}));
});
}),
];
MSW vs Other Mocking Approaches
| MSW | Jest mocks | Nock | JSON Server | |
|---|---|---|---|---|
| Level | Network | Module | HTTP | Server |
| Browser | Yes | No | No | Yes |
| Node.js | Yes | Yes | Yes | Yes |
| Framework agnostic | Yes | Jest only | Node only | Yes |
| WebSocket | Yes | No | No | No |
| No code changes | Yes | Requires import changes | Yes | Yes |
Getting Started
npm install msw --save-dev
npx msw init public/ --save
Create src/mocks/handlers.ts, define your handlers, and you're done.
The Bottom Line
MSW is the standard for API mocking in 2026. Network-level interception, cross-environment support, and WebSocket mocking make it the tool your test suite has been missing.
Building data-intensive applications? I create custom web scraping and data extraction tools. Check out my Apify actors or email spinov001@gmail.com.
Top comments (0)