Skip to main content

Guide

MSW vs Nock vs axios-mock-adapter 2026

Compare MSW (Mock Service Worker), Nock, and axios-mock-adapter for API mocking in JavaScript tests. Approach, browser support, type safety, and which to use.

·PkgPulse Team·
0

TL;DR

MSW (Mock Service Worker) is the modern standard for API mocking — it intercepts at the network level (not the library level), works in both browser and Node.js, and its mocks can be reused across unit tests, integration tests, and development. Nock is the Node.js HTTP interception classic — powerful but Node-only. axios-mock-adapter is convenient if you're already on Axios but locks your tests to Axios's implementation.

Key Takeaways

  • MSW: ~2.5M weekly downloads — network-level mocking, browser + Node, the 2026 standard
  • Nock: ~4.2M weekly downloads — Node.js HTTP interception, widely used in existing codebases
  • axios-mock-adapter: ~1.8M weekly downloads — Axios-specific, adapter-level mocking
  • MSW works with any fetch-based API: Axios, fetch, TanStack Query, SWR, Apollo
  • Nock is excellent but incompatible with browser environments and native fetch by default
  • MSW wins for new projects: shared mocks between dev, integration, and unit tests

PackageWeekly DownloadsWorks InLevel
nock~4.2MNode.js onlyHTTP module interception
axios-mock-adapter~1.8MNode + BrowserAxios adapter
msw~2.5MNode + BrowserService Worker / fetch

Nock's high download count reflects its legacy use in Node.js test suites — MSW has overtaken it in new projects.


The Mocking Hierarchy

Understanding where each library intercepts helps choose the right one:

Browser/Node Request Flow:
  Application Code
       ↓
  HTTP Client (Axios, fetch, got)
       ↓
  [axios-mock-adapter intercepts here — Axios adapter only]
       ↓
  Node.js http/https module
       ↓
  [Nock intercepts here — Node.js only, all http clients]
       ↓
  [MSW intercepts here via Service Worker (browser) or http-interceptor (Node)]
       ↓
  Network (real HTTP call)

MSW intercepts lowest — it works regardless of which HTTP library you use.


MSW (Mock Service Worker)

MSW defines request handlers that intercept at the network level:

// src/mocks/handlers.ts — Define handlers once, use everywhere
import { http, HttpResponse } from "msw"

export const handlers = [
  // Match any GET to /api/packages/:name
  http.get("/api/packages/:name", ({ params }) => {
    const { name } = params
    return HttpResponse.json({
      name,
      version: "18.2.0",
      downloads: 25000000,
      description: "A JavaScript library for building user interfaces",
    })
  }),

  // POST with request body parsing:
  http.post("/api/packages/compare", async ({ request }) => {
    const { packages } = await request.json() as { packages: string[] }

    if (packages.length < 2) {
      return HttpResponse.json(
        { error: "At least 2 packages required" },
        { status: 400 }
      )
    }

    return HttpResponse.json({
      packages: packages.map((name) => ({ name, downloads: Math.random() * 10000000 }))
    })
  }),

  // Simulate network error:
  http.get("/api/packages/bad-package", () => {
    return HttpResponse.error()  // Simulates a network failure
  }),

  // Passthrough — don't mock this URL:
  http.get("/api/health", ({ request }) => {
    return fetch(request)  // Let it through to real network
  }),
]

Browser setup (Service Worker):

// src/mocks/browser.ts
import { setupWorker } from "msw/browser"
import { handlers } from "./handlers"

export const worker = setupWorker(...handlers)

// In your app entry point (development only):
// src/main.tsx
if (process.env.NODE_ENV === "development") {
  const { worker } = await import("./mocks/browser")
  await worker.start({
    onUnhandledRequest: "bypass",  // Don't warn on real API calls
  })
}
# One-time setup — generates service worker file:
npx msw init public/

Node.js setup (for Vitest/Jest):

// src/mocks/server.ts
import { setupServer } from "msw/node"
import { handlers } from "./handlers"

export const server = setupServer(...handlers)

// vitest.setup.ts or jest.setup.ts:
import { server } from "./src/mocks/server"

beforeAll(() => server.listen({ onUnhandledRequest: "error" }))
afterEach(() => server.resetHandlers())  // Reset per-test overrides
afterAll(() => server.close())

Per-test handler overrides:

import { server } from "../mocks/server"
import { http, HttpResponse } from "msw"

test("handles 404 from npm registry", async () => {
  // Override for just this test:
  server.use(
    http.get("/api/packages/:name", () => {
      return HttpResponse.json({ error: "Not found" }, { status: 404 })
    })
  )

  render(<PackagePage name="nonexistent-package-xyz" />)
  await screen.findByText("Package not found")
})

MSW with TanStack Query:

// MSW intercepts at network level — TanStack Query never knows it's mocked:
function PackagePage({ name }: { name: string }) {
  const { data, isLoading, isError } = useQuery({
    queryKey: ["package", name],
    queryFn: () => fetch(`/api/packages/${name}`).then(r => r.json()),
  })
  // ...
}

// In tests — just render the component, MSW handles the rest:
test("shows package data", async () => {
  render(<QueryClientProvider client={new QueryClient()}><PackagePage name="react" /></QueryClientProvider>)
  await screen.findByText("A JavaScript library for building user interfaces")
})

Nock

Nock intercepts Node.js's http and https modules directly:

import nock from "nock"
import axios from "axios"
import { got } from "got"

// Intercept a specific URL:
nock("https://registry.npmjs.org")
  .get("/react")
  .reply(200, {
    name: "react",
    "dist-tags": { latest: "18.2.0" },
    description: "React is a JavaScript library for building user interfaces.",
  })

// Works with any Node.js HTTP client (axios, got, node-fetch, etc.):
const response = await axios.get("https://registry.npmjs.org/react")
console.log(response.data.name)  // "react"

// Nock intercept scopes:
const scope = nock("https://api.pkgpulse.com")
  .get("/packages/react")
  .reply(200, { name: "react", downloads: 25000000 })
  .get("/packages/vue")
  .reply(200, { name: "vue", downloads: 7000000 })

await scope.isDone()  // Verify all interceptors were used

Nock request matching:

// Match with query parameters:
nock("https://api.example.com")
  .get("/search")
  .query({ q: "react", limit: "10" })
  .reply(200, searchResults)

// Match POST body:
nock("https://api.example.com")
  .post("/packages", { name: "test-pkg", version: "1.0.0" })
  .reply(201, { id: "pkg_123", ...publishedData })

// Headers matching:
nock("https://api.example.com", {
  reqheaders: { Authorization: /Bearer .+/, "Content-Type": "application/json" }
})
  .get("/profile")
  .reply(200, userProfile)

// Delay response (test loading states):
nock("https://api.example.com")
  .get("/slow-endpoint")
  .delay(2000)
  .reply(200, data)

// Conditional reply:
nock("https://api.example.com")
  .get("/packages/react")
  .times(1).reply(503)  // First call fails
  .get("/packages/react")
  .reply(200, data)      // Second call succeeds (test retry logic)

Nock limitations:

// Problem 1: Doesn't work with native fetch in Node 18+ by default
// Solution: Use nock with fetch polyfill or switch to MSW

// Problem 2: Node-only — can't share mocks between browser tests
// Problem 3: Intercepts node's http module — some newer clients bypass it

// After each test, clean up:
afterEach(() => nock.cleanAll())
afterAll(() => nock.restore())  // Restore http module

axios-mock-adapter

axios-mock-adapter intercepts at the Axios adapter level:

import axios from "axios"
import MockAdapter from "axios-mock-adapter"

const mock = new MockAdapter(axios, { delayResponse: 100 })

// GET request:
mock.onGet("/api/packages/react").reply(200, {
  name: "react",
  downloads: 25000000,
})

// POST with request body match:
mock.onPost("/api/packages/compare", { packages: ["react", "vue"] }).reply(200, {
  comparison: compareData,
})

// Network error simulation:
mock.onGet("/api/packages/error").networkError()

// Timeout simulation:
mock.onGet("/api/slow").timeout()

// Pass through to real network:
mock.onGet("/api/health").passThrough()

// Reset all mocks:
mock.reset()

// Clean up after tests:
afterEach(() => mock.reset())
afterAll(() => mock.restore())  // Restores original Axios adapter

Per-instance mocking (more granular):

import axios from "axios"
import MockAdapter from "axios-mock-adapter"

// Mock only a specific Axios instance:
const apiClient = axios.create({ baseURL: "https://api.pkgpulse.com" })
const mock = new MockAdapter(apiClient)

mock.onGet("/packages/react").reply(200, reactData)

// Other axios instances are NOT affected

Feature Comparison

FeatureMSWNockaxios-mock-adapter
Works in browser✅ Service Worker
Works in Node.js✅ http-interceptor✅ (Node + Browser)
HTTP client agnostic✅ (Node http)❌ Axios only
native fetch support
Shared browser/test mocks
Request body matching
Response delay simulation
Network error simulation
TypeScript
Passthrough to real API
Dev mode (not just tests)✅ Service Worker

Why MSW is the 2026 Standard

MSW's killer feature: one set of mocks for everything:

handlers.ts  ─────────────────────────────────────────────┐
     │                                                      │
     ├── Development (Service Worker)                       │
     │   └── Frontend dev without backend running           │
     │                                                      │
     ├── Component Tests (Vitest + Node server)             │
     │   └── Testing components that fetch data             │
     │                                                      │
     ├── Integration Tests                                  │
     │   └── Testing full request/response cycles           │
     │                                                      │
     └── Storybook (browser environment)                    │
         └── Stories with data-fetching components         ─┘

Write your API mocks once, use them in 4 different contexts.


When to Use Each

Choose MSW if:

  • Starting a new project in 2026
  • You want to reuse mocks in development AND tests
  • Your app uses native fetch, TanStack Query, SWR, or Apollo
  • You test components in both JSDOM (Vitest) and real browsers (Playwright)

Choose Nock if:

  • Node.js-only backend testing (API routes, services)
  • You need fine-grained HTTP module interception
  • Your existing test suite already uses Nock

Choose axios-mock-adapter if:

  • Your app is already deeply committed to Axios
  • You want to mock specific Axios instances (not global)
  • Quick addition to an existing Axios-based test suite

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on MSW v2.x, Nock 14.x, and axios-mock-adapter 2.x documentation.

Testing React Components That Fetch Data

The most practical argument for MSW over Nock or axios-mock-adapter is how they interact with component testing. Nock intercepts Node.js's http module, which means it works when your test calls a function that makes an HTTP request internally. But when you're testing a React component that uses useQuery, useSWR, or even a bare fetch call, Nock intercepts at the wrong layer — it catches the request made by your component's internal useEffect, but the interception isn't visible to the test at the component level. This creates invisible coupling between your component's implementation and your test setup.

MSW's approach is different: because it intercepts at the network boundary using a service worker in the browser and an http-interceptor in Node.js, your test code has no idea which HTTP library the component uses. You test the component's rendered output — loading states, error states, success states — and MSW silently provides the data. Swap your component from fetch to axios or TanStack Query, and the MSW handlers don't need to change. This is the design property that makes MSW the standard for component testing in 2026.

When testing with React Testing Library, the pattern is to render the component, wait for the async state to resolve, and assert on the DOM. MSW's server.use() override pattern lets you set up the happy path in your default handlers and override for specific test cases — 404s, 500s, slow responses, network errors — all within the test file itself without any shared mutable state.

Handling Authentication and Headers in Tests

A recurring challenge with API mocking is testing code that attaches authentication headers — JWT tokens, API keys, or session cookies. Each library handles this differently. Nock's reqheaders matching lets you assert that specific headers were sent, making it the most precise tool for testing header logic in pure Node.js code:

nock("https://api.example.com", {
  reqheaders: { Authorization: /^Bearer [A-Za-z0-9._-]+$/ },
})
  .get("/protected")
  .reply(200, data)

MSW takes a different approach — request handlers receive the full Request object including headers, so you can inspect them within the handler and return different responses conditionally. For component tests, this matters when testing how your UI handles 401 responses versus 403 responses. axios-mock-adapter can match on request config including headers, but since it only intercepts Axios, any code that doesn't use Axios won't be covered.

The important security consideration: don't assert that specific token values appear in tests — tokens should be opaque in test fixtures. Instead, assert that some Authorization header is present (Nock's regex match) or that the component redirects to login on a 401 (MSW's conditional response). Testing the presence of authentication structure, not the token value, keeps tests resilient as token formats evolve.

Migrating from Nock to MSW

Teams migrating existing Nock suites to MSW face a structural difference: Nock interceptors are defined per-test and consumed once, while MSW handlers are defined globally and reset between tests. This means migration isn't a line-for-line translation — it requires thinking about which mocks are universal (go in handlers.ts) versus test-specific (go in server.use() overrides).

The migration pays off when a codebase has both browser and Node.js tests that currently maintain two sets of mocks. MSW consolidates them: the same handlers.ts file powers msw/browser for Storybook and dev mode and msw/node for Vitest and integration tests. Teams report that after migration they find duplicate mocks that were subtly inconsistent — the browser mock returning one shape and the Nock mock returning another.

Integration with Component Testing Workflows

The shift toward component-level testing with Vitest and Testing Library changes the practical requirements for API mocking. When a component that fetches data is rendered in a test, the HTTP request happens inside the component's lifecycle — not in a place where you can easily intercept it with a sinon stub or axios-mock-adapter. MSW's network-level interception is the only approach that works transparently here: the component makes the real fetch call, MSW intercepts it at the network boundary, and the component receives the mocked response as if it came from the server. This enables testing the full component lifecycle — loading state, success state, error state, empty state — with a realistic data flow rather than mocking internal implementation details. The setup requires setupServer() in your Vitest configuration and server.use() in tests that need to deviate from the default handlers. Once configured, component tests that rely on API data are as simple as rendering the component and asserting on the resulting DOM — the HTTP mock is handled at a layer below the component's awareness.

Mocking GraphQL APIs and WebSocket Connections

Beyond REST endpoints, modern applications use GraphQL and WebSocket connections that have different mocking requirements. MSW has first-class GraphQL support via its graphql handler: graphql.query('GetUser', ({ variables }) => HttpResponse.json({ data: { user: { id: variables.id, name: 'Alice' } } })) intercepts named GraphQL queries regardless of the endpoint URL. The handler matches on the operation name, not the URL, which is the correct model for GraphQL where all queries go to a single endpoint. WebSocket mocking in MSW became available in v2.3+, with ws.link() handlers that intercept WebSocket connections and emit events programmatically — enabling testing of real-time features like chat, live data feeds, and collaborative editing without running a real WebSocket server. Nock's HTTP module interception means it cannot intercept WebSocket connections, which operate via the upgrade mechanism. axios-mock-adapter is irrelevant for both GraphQL (which uses fetch or a dedicated client) and WebSockets. For applications that heavily use GraphQL or WebSockets, MSW's multi-protocol support is a significant architectural advantage.

Compare testing library packages on PkgPulse →

See also: Axios vs node-fetch and Axios vs Ky, @faker-js/faker vs Chance.js vs Casual.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.