Skip to main content

Best API Mocking Libraries for JavaScript Testing 2026

·PkgPulse Team
0

TL;DR

MSW (Mock Service Worker) is the 2026 standard for API mocking. MSW (~5M weekly downloads) intercepts requests at the network level using Service Workers (browser) or Node.js interceptors — your code never knows it's mocked. nock (~8M downloads) is Node.js-only and intercepts http/https module calls. Mirage.js (~400K) is browser-focused with a fake database layer. For most projects, MSW handles both browser and Node.js test environments seamlessly.

Key Takeaways

  • MSW: ~5M weekly downloads — browser + Node.js, network-level interception, no code changes
  • nock: ~8M downloads — Node.js only, intercepts http.request, widely used in legacy code
  • Mirage: ~400K downloads — browser-only, in-memory DB, REST + GraphQL
  • MSW v2 — native fetch interception, TypeScript-first, no polyfills needed
  • @mswjs/data — MSW companion for typed fake database

MSW: The 2026 Standard

MSW occupies a rare position: it's simultaneously the most powerful and the most non-invasive mocking tool in the JavaScript ecosystem. The core insight is that the ideal place to intercept HTTP requests is at the network level, not at the module level. Your application code calls fetch('/api/users') the same way in tests as in production. MSW intercepts the actual network request — via a Service Worker in the browser, via a Node.js request interceptor in tests — and returns whatever your handler specifies.

This means you can add MSW to an existing project without changing a single line of application code. No dependency injection, no module mocking, no jest.mock() wrapping your fetch calls. The test environment becomes realistic because the code path to the network is real.

MSW v2 API

MSW v2 (released in late 2023) brought a significant API revision. The rest.get / rest.post namespace became http.get / http.post, and the response format shifted from a custom res(ctx.json(...)) composition to a standard HttpResponse API that mirrors the Fetch API's Response. The v2 API is cleaner and the TypeScript types are first-class.

// MSW v2 — define handlers once, use in browser + tests
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  // GET handler
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
    ]);
  }),

  // GET with path params
  http.get('/api/users/:id', ({ params }) => {
    const { id } = params;
    return HttpResponse.json({
      id: Number(id),
      name: 'Alice',
      email: 'alice@example.com',
    });
  }),

  // POST handler — read request body
  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      { id: Date.now(), ...body },
      { status: 201 }
    );
  }),

  // Error simulation
  http.delete('/api/users/:id', () => {
    return HttpResponse.json(
      { message: 'Unauthorized' },
      { status: 401 }
    );
  }),
];

Browser Setup

In the browser, MSW registers a Service Worker (a file you generate once with npx msw init public/) that intercepts all fetch requests matching your handlers. Development mode gets realistic API responses without spinning up a real backend:

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

export const worker = setupWorker(...handlers);

// main.tsx — start worker in development
if (process.env.NODE_ENV === 'development') {
  const { worker } = await import('./mocks/browser');
  await worker.start({ onUnhandledRequest: 'bypass' });
}

Vitest / Jest Setup

In Node.js test environments, MSW uses request interception without Service Workers. The setupServer() function returns a server object whose lifecycle you manage in your test setup file:

// 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());

Setting onUnhandledRequest: 'error' during tests is important — it causes unmatched requests to throw, which catches missing handler definitions early rather than silently returning undefined.

Per-Test Handler Overrides

The server.use() method lets individual tests temporarily override specific handlers. This is how you test error states and loading states without maintaining separate handler files:

// Override handlers in individual tests
import { server } from '../mocks/server';
import { http, HttpResponse, delay } from 'msw';

describe('UserProfile', () => {
  it('shows loading state', async () => {
    server.use(
      http.get('/api/users/:id', async () => {
        await delay(500); // Simulate slow network
        return HttpResponse.json({ id: 1, name: 'Alice' });
      })
    );

    render(<UserProfile userId={1} />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  it('shows error state', async () => {
    server.use(
      http.get('/api/users/:id', () => {
        return HttpResponse.json(
          { message: 'Not found' },
          { status: 404 }
        );
      })
    );

    render(<UserProfile userId={999} />);
    await waitFor(() => {
      expect(screen.getByText('User not found')).toBeInTheDocument();
    });
  });
});

Per-test overrides are reset by server.resetHandlers() in afterEach, so they don't bleed into other tests.

MSW + @mswjs/data: Typed Mock Database

For complex test suites, @mswjs/data provides an in-memory database that generates MSW handlers automatically:

// @mswjs/data — in-memory typed database for MSW
import { factory, primaryKey, nullable } from '@mswjs/data';

// Define your data model
const db = factory({
  user: {
    id: primaryKey(String),
    name: String,
    email: String,
    role: String,
    createdAt: nullable(Date),
  },
  post: {
    id: primaryKey(String),
    title: String,
    body: String,
    authorId: String,
  },
});

// Seed data
db.user.create({ id: '1', name: 'Alice', email: 'alice@example.com', role: 'admin' });
db.user.create({ id: '2', name: 'Bob', email: 'bob@example.com', role: 'user' });

// Generate REST handlers automatically from the model
export const handlers = [
  ...db.user.toHandlers('rest', 'https://api.example.com'),
  // Auto-generates: GET /users, GET /users/:id, POST /users, PATCH /users/:id, DELETE /users/:id
];

This makes tests that require reading state (asserting a user was created, listing records after a delete) much cleaner than static handler responses.


nock: Legacy Node.js Testing

nock (~8M downloads) predates MSW by several years and remains the most widely used HTTP mocking library in Node.js — especially in older codebases. It intercepts requests at the http and https module level, which means it works regardless of whether your code uses node-fetch, axios, got, or the built-in http module directly.

The API is chainable and declarative. You specify the base URL, the path, optional query parameters and request body matchers, and the desired response:

// npm install nock
import nock from 'nock';
import axios from 'axios';

describe('UserService', () => {
  afterEach(() => nock.cleanAll());

  it('fetches a user', async () => {
    nock('https://api.example.com')
      .get('/users/1')
      .reply(200, { id: 1, name: 'Alice' });

    const user = await getUser(1); // getUser uses axios internally
    expect(user.name).toBe('Alice');
    // nock verifies the request was made
  });

  it('handles network errors', async () => {
    nock('https://api.example.com')
      .get('/users/1')
      .replyWithError('Network failure');

    await expect(getUser(1)).rejects.toThrow('Network failure');
  });

  it('handles delayed responses', async () => {
    nock('https://api.example.com')
      .get('/users')
      .delay(200)
      .reply(200, []);

    const users = await listUsers();
    expect(users).toEqual([]);
  });
});

nock supports matching on query parameters, request headers, and request bodies with either exact values or regular expressions:

// nock — with query params and headers
nock('https://api.example.com')
  .get('/users')
  .query({ page: '1', limit: '10' })
  .matchHeader('Authorization', /^Bearer .+/)
  .reply(200, { users: [], total: 0 });

// POST with body matching
nock('https://api.example.com')
  .post('/users', { name: 'Alice', email: /alice@/ })
  .reply(201, { id: 123 });

The primary limitation of nock in 2026 is that it does not intercept the native fetch API. If your codebase uses Node.js 18+ native fetch (which most modern projects do), nock won't intercept those calls without configuration changes. MSW v2 natively handles fetch interception in Node.js, which is the main reason MSW has overtaken nock for new projects.

For legacy Node.js services that rely on axios, node-fetch v2, or the built-in http module, nock remains a solid choice. The test syntax is concise and there is zero setup overhead — just install and start mocking.


Mirage.js: Browser Prototyping

Mirage.js (~400K downloads) takes a different conceptual approach. Rather than intercepting real network requests, Mirage.js creates a complete in-memory fake server with its own database, relationships, serializers, and route definitions. It runs entirely in the browser using Pretender (an XMLHttpRequest interceptor) and is designed for building out front-end features before a real API exists.

The Mirage server has a routes() function where you define endpoints, a models() function where you define data relationships, and a seeds() function for test data:

// Mirage.js — complete fake server in the browser
import { createServer, Model, belongsTo, hasMany } from 'miragejs';

createServer({
  models: {
    user: Model.extend({
      posts: hasMany(),
    }),
    post: Model.extend({
      author: belongsTo('user'),
    }),
  },

  seeds(server) {
    const alice = server.create('user', { name: 'Alice', role: 'admin' });
    server.create('post', { title: 'Hello World', author: alice });
    server.create('post', { title: 'Second Post', author: alice });
    server.create('user', { name: 'Bob', role: 'user' });
  },

  routes() {
    this.namespace = 'api';

    this.get('/users', (schema) => {
      return schema.users.all();
    });

    this.get('/users/:id', (schema, request) => {
      return schema.users.find(request.params.id);
    });

    this.post('/users', (schema, request) => {
      const attrs = JSON.parse(request.requestBody);
      return schema.users.create(attrs);
    });

    this.get('/users/:id/posts', (schema, request) => {
      const user = schema.users.find(request.params.id);
      return user.posts;
    });
  },
});

Mirage's strength is that it maintains relational state across requests. Creating a user returns a real record that a subsequent GET request will return. This is closer to how a real API behaves than static handler responses. For building complete UI flows during prototyping — forms, navigation, pagination — Mirage's persistence makes the experience realistic.

The trade-off is that Mirage doesn't work in Node.js test environments (it relies on browser XMLHttpRequest), and the in-memory database approach can be overkill when you just need a quick mock response for a unit test. As MSW has grown its @mswjs/data companion, the use case gap between MSW and Mirage has narrowed significantly.


Comparison Table

LibraryBrowserNode.jsGraphQLHandler StyleBundle Size
MSWYesYesYeshttp.get('/path', resolver)~35KB
nockNoYesPartial.get('/path').reply(200, data)~50KB
Mirage.jsYesNoYesthis.get('/api/users', fn)~100KB

When to Choose

Choose MSW for any new project and for most existing ones. It handles both browser and Node.js environments with the same handler definitions, the v2 API is clean and TypeScript-native, and it integrates naturally with Vitest, Jest, and Playwright. The fact that your application code runs unchanged through real network paths means your tests are more realistic than module-level mocking provides. MSW is the answer unless you have a specific constraint.

Choose nock if you're working on a legacy Node.js service that doesn't use native fetch, or if you have an extensive existing nock test suite that isn't worth migrating. nock's chainable API is concise for simple mocking scenarios, and it requires zero setup beyond installation. The ecosystem of nock-based tests is large enough that you'll encounter it frequently in established codebases.

Choose Mirage.js for browser-only prototyping when you want to build a full front-end flow before the API exists. The relational in-memory database makes multi-step user flows work correctly — you can create a record, navigate to a list view, and see it appear without any server. For greenfield front-end development where backend work is lagging, Mirage is the most productive prototyping tool.


Integrating API Mocking into a Real Test Suite

MSW with React Testing Library

The most common production setup in 2026 combines MSW with React Testing Library and Vitest. The pattern works like this: your vitest.setup.ts (or jest.setup.ts) file manages the server lifecycle globally, individual test files use server.use() for per-test overrides, and your handlers file contains the "happy path" responses that most tests expect.

This approach makes tests concise. The majority of tests that just need a user to exist don't have to set up any mock — the global handler returns a default user. Only tests specifically testing error states or edge cases set up overrides.

A production vitest configuration looks like this:

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';
import { server } from './mocks/server';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

The onUnhandledRequest: 'error' setting is important in a CI environment. It causes tests to fail if they make a request to a path that has no handler, which surfaces missing mock definitions early rather than silently returning undefined responses.

GraphQL Mocking with MSW

MSW handles GraphQL alongside REST. The graphql.query() and graphql.mutation() handlers intercept requests to your GraphQL endpoint by operation name:

import { graphql, HttpResponse } from 'msw';

export const handlers = [
  graphql.query('GetUser', ({ variables }) => {
    return HttpResponse.json({
      data: {
        user: {
          id: variables.id,
          name: 'Alice',
          email: 'alice@example.com',
        },
      },
    });
  }),

  graphql.mutation('CreateUser', ({ variables }) => {
    return HttpResponse.json({
      data: {
        createUser: {
          id: 'new-id',
          ...variables.input,
        },
      },
    });
  }),
];

This works regardless of whether your GraphQL client is Apollo Client, urql, or a raw fetch. Because MSW intercepts at the network level, the GraphQL client operates exactly as it does in production.

Mocking Network Errors and Edge Cases

Testing error handling is where API mocking earns its value. Testing with a real API makes error scenarios brittle — you can't reliably reproduce a 500 error or a network timeout. With MSW, any scenario is repeatable:

import { http, HttpResponse, delay, NetworkError } from 'msw';

// Simulate a server error
server.use(
  http.get('/api/users', () => {
    return HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 });
  })
);

// Simulate a network failure (fetch throws)
server.use(
  http.get('/api/users', () => {
    throw new NetworkError('Failed to fetch');
  })
);

// Simulate a slow response for loading state tests
server.use(
  http.get('/api/users', async () => {
    await delay(2000);
    return HttpResponse.json([]);
  })
);

// Simulate rate limiting
server.use(
  http.post('/api/users', () => {
    return HttpResponse.json(
      { message: 'Too many requests' },
      { status: 429, headers: { 'Retry-After': '60' } }
    );
  })
);

Each of these scenarios maps to a real failure mode your application will encounter in production. Building explicit tests for them ensures your error states, loading states, and retry logic actually work before you deploy.

Moving from jest.mock() to MSW

A common migration scenario is replacing jest.mock() module-level mocking with MSW network-level mocking. The module-level approach mocks the fetch or HTTP client module itself:

// Old approach — mocking the module
jest.mock('../api/users', () => ({
  getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));

This works but has a significant downside: the mock bypasses the actual HTTP client code, including any auth headers, error handling, timeout logic, or retry behavior in your API layer. If a bug lives in that layer, module-level mocking won't catch it.

MSW network-level mocking runs your application's actual fetch code. The request goes through your real API client with real headers, through real error handling, and only at the actual network boundary does MSW intercept it. This means your tests exercise more of the real code path and catch more real bugs.

The migration approach is straightforward: write MSW handlers that return the same data your jest.mock() was returning, remove the jest.mock() calls, and watch your tests continue passing — now with better coverage.


MSW in Playwright E2E Tests

MSW works beyond unit tests — it can be used inside Playwright E2E test runs to control the API layer. The approach is to start your application with MSW active (pointing the browser at localhost with the Service Worker registered), then use Playwright to interact with the UI while MSW controls the responses.

This pattern is useful for testing flows that depend on specific API states that are hard to reproduce with a real backend: testing what happens when a payment fails, when a user's session expires mid-flow, or when the API returns an empty list. With MSW, you can script these scenarios reliably in CI.

The alternative is to mock at the Playwright network layer using page.route(), which intercepts requests in the browser context. This doesn't require MSW but means duplicating your mock definitions — you'd have MSW handlers for unit tests and page.route() handlers for E2E tests. For teams that want a single mock definition shared across both, running MSW inside Playwright is the cleaner approach.

Storybook Integration

MSW also integrates with Storybook through the msw-storybook-addon package. Each Story can define its own MSW handlers via the parameters.msw.handlers property, allowing you to build out component stories that demonstrate specific API states — loading, error, empty, populated — without needing a real backend.

This is particularly valuable for component libraries and design systems. A UserCard component story can show the card in its loading state (MSW returns a slow response), its error state (MSW returns 404), and its success state (MSW returns full user data) — all runnable in isolation in Storybook without backend coordination.

fetch-mock: A Lightweight Alternative

For projects that need simple fetch mocking without the Service Worker setup or the full MSW infrastructure, fetch-mock is a lighter alternative. It patches the global fetch function directly and supports a simple mockFetch.get('/api/users', [...data]) syntax. It works in both browser and Node.js environments and requires zero build configuration.

The trade-off is that fetch-mock mocks the fetch function rather than the network — meaning it bypasses the actual fetch call entirely. This is closer to module-level mocking than network-level mocking. For most unit tests this doesn't matter, but for integration tests where you want to verify headers, authentication, or retry logic in your fetch wrapper, MSW's network-level approach is more thorough.


See the live comparison

View msw vs. nock on PkgPulse →

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.