Skip to main content

MSW vs Nock vs axios-mock-adapter: API Mocking in Tests (2026)

·PkgPulse Team

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.

Compare testing library packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.