Skip to main content

Best npm Packages for API Testing and Mocking in 2026

·PkgPulse Team
0

MSW (Mock Service Worker) intercepts requests at the network level — the same mock works in your browser, Node.js tests, and React Native. Supertest lets you test your Express or Fastify routes without spinning up a real server. Nock intercepts Node.js HTTP requests at the module level. These tools solve different layers of API testing: mocking external APIs vs. testing your own API routes vs. intercepting outbound HTTP.

TL;DR

MSW for mocking third-party APIs in tests and development — the 2026 standard for isomorphic HTTP mocking. Supertest for integration testing your own API routes — fast, simple, no server needed. Nock for intercepting HTTP calls in Node.js tests when MSW is overkill. Playwright for full end-to-end API testing with real HTTP. The right choice depends on what you're testing: your API or someone else's.

Key Takeaways

  • MSW: 5M weekly downloads, intercepts at service worker/Node.js http module level, isomorphic
  • Supertest: 8M weekly downloads, tests Express/Fastify/Hono routes without a real server
  • Nock: 6M weekly downloads, Node.js HTTP interceptor, works with any HTTP library
  • MSW v2: Native fetch interception, Node.js 18+ compatible, no polyfills needed
  • Supertest: Uses the same expect API as Jest/Vitest, chainable assertions
  • All tools: Work alongside Jest, Vitest, or any test runner

The API Testing Landscape

API testing in Node.js apps has two distinct problems:

  1. Testing your own API: Does /api/users return the right data?
  2. Testing code that calls external APIs: Does your service correctly handle Stripe, SendGrid, GitHub API responses?

Different tools solve different parts of this:

Your API routes → Supertest
External API calls (HTTP) → MSW or Nock
Full browser + network → Playwright
Type-safe API contracts → ts-rest + OpenAPI

MSW (Mock Service Worker)

Package: msw Weekly downloads: 5M GitHub stars: 16K Creator: Artem Zakharchenko

MSW is the most sophisticated HTTP mocking library. It intercepts requests at the network level — using a Service Worker in the browser and Node.js's http module in tests — meaning you mock at the same level the browser does.

Installation

npm install -D msw
# For browser usage:
npx msw init public/ --save

Defining Handlers

// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

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

  // Mock POST /api/users
  http.post('/api/users', async ({ request }) => {
    const body = await request.json() as { name: string; email: string };
    return HttpResponse.json(
      { id: '3', ...body },
      { status: 201 }
    );
  }),

  // Mock external API (Stripe)
  http.post('https://api.stripe.com/v1/charges', () => {
    return HttpResponse.json({
      id: 'ch_test_123',
      status: 'succeeded',
      amount: 2000,
    });
  }),

  // Simulate error
  http.get('/api/products/:id', ({ params }) => {
    if (params.id === '999') {
      return new HttpResponse(null, { status: 404 });
    }
    return HttpResponse.json({ id: params.id, name: 'Widget' });
  }),
];

Node.js Setup (Tests)

// src/mocks/node.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
// vitest.setup.ts / jest.setup.ts
import { server } from './src/mocks/node';

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

Using MSW in Tests

// payment.service.test.ts
import { describe, it, expect } from 'vitest';
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/node';
import { PaymentService } from './payment.service';

describe('PaymentService', () => {
  it('processes payment successfully', async () => {
    // Handlers set up in vitest.setup.ts handle this automatically
    const service = new PaymentService();
    const result = await service.charge({ amount: 2000, currency: 'usd' });

    expect(result.status).toBe('succeeded');
    expect(result.id).toBe('ch_test_123');
  });

  it('handles payment failure', async () => {
    // Override handler for this test
    server.use(
      http.post('https://api.stripe.com/v1/charges', () => {
        return HttpResponse.json(
          { error: { message: 'Card declined' } },
          { status: 402 }
        );
      })
    );

    const service = new PaymentService();
    await expect(service.charge({ amount: 2000, currency: 'usd' }))
      .rejects.toThrow('Card declined');
  });
});

Browser Setup (Development)

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

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

In development, every API call is intercepted by the Service Worker — no backend needed for frontend development.

MSW v2 Key Features

  • Native fetch: Intercepts fetch() natively without polyfills (Node.js 18+)
  • WebSocket mocking: Mock WebSocket connections (v2.3+)
  • GraphQL support: graphql.query, graphql.mutation handlers
  • Request passthrough: passthrough() for letting specific requests reach the real server

MSW Strengths

  • Same handlers work in browser, Node.js, and React Native
  • Intercepts at network level — your code doesn't know it's mocked
  • Excellent DX: readable handler definitions, TypeScript support
  • Works with any HTTP library (fetch, axios, ky, got, node-fetch)

Supertest

Package: supertest Weekly downloads: 8M GitHub stars: 14K

Supertest lets you test HTTP servers (Express, Fastify, Hono, Koa) without starting a real server. It binds to a random port, sends requests, and returns typed responses.

Installation

npm install -D supertest @types/supertest

Testing Express Routes

// app.ts
import express from 'express';

const app = express();
app.use(express.json());

app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) return res.status(404).json({ message: 'Not found' });
  res.json(user);
});

app.post('/users', async (req, res) => {
  const user = await db.users.create(req.body);
  res.status(201).json(user);
});

export default app;
// app.test.ts
import request from 'supertest';
import app from './app';

describe('Users API', () => {
  it('GET /users/:id returns user', async () => {
    const response = await request(app)
      .get('/users/123')
      .expect(200)                         // Status assertion
      .expect('Content-Type', /json/);     // Header assertion

    expect(response.body.name).toBe('Alice');
    expect(response.body.id).toBe('123');
  });

  it('GET /users/:id returns 404 for unknown id', async () => {
    await request(app)
      .get('/users/9999')
      .expect(404)
      .expect({ message: 'Not found' });
  });

  it('POST /users creates user', async () => {
    const response = await request(app)
      .post('/users')
      .send({ name: 'Bob', email: 'bob@example.com' })
      .set('Authorization', 'Bearer test-token')
      .expect(201);

    expect(response.body.id).toBeDefined();
    expect(response.body.name).toBe('Bob');
  });

  it('POST /users validates input', async () => {
    await request(app)
      .post('/users')
      .send({ name: '' })  // Missing email
      .expect(400);
  });
});

Supertest with Supertest Session (Auth)

import request from 'supertest-session';

const session = request(app);

it('authenticated routes work', async () => {
  // Login first
  await session
    .post('/auth/login')
    .send({ email: 'admin@example.com', password: 'password' })
    .expect(200);

  // Session maintained across requests
  await session
    .get('/admin/users')
    .expect(200);
});

Supertest Strengths

  • No server needed — works on the Express app object directly
  • Chainable assertions (.expect(200).expect('Content-Type', /json/))
  • Works with any Node.js HTTP framework
  • Simple API — minimal learning curve
  • 8M weekly downloads — most-used HTTP testing library

Supertest Limitations

  • Node.js only (not browser)
  • Tests your API implementation, not external API behavior
  • No built-in mock for external dependencies (combine with MSW/Nock)

Nock

Package: nock Weekly downloads: 6M GitHub stars: 12.5K

Nock intercepts Node.js's http and https modules, blocking real HTTP calls and returning mocked responses.

Installation

npm install -D nock

Basic Usage

import nock from 'nock';

describe('GitHub integration', () => {
  afterEach(() => {
    nock.cleanAll();
  });

  it('fetches repositories', async () => {
    // Intercept: GET https://api.github.com/users/octocat/repos
    nock('https://api.github.com')
      .get('/users/octocat/repos')
      .reply(200, [
        { id: 1, name: 'Hello-World', full_name: 'octocat/Hello-World' },
      ]);

    const repos = await githubClient.getUserRepos('octocat');
    expect(repos).toHaveLength(1);
    expect(repos[0].name).toBe('Hello-World');
  });

  it('handles API rate limiting', async () => {
    nock('https://api.github.com')
      .get('/users/octocat/repos')
      .reply(403, { message: 'API rate limit exceeded' });

    await expect(githubClient.getUserRepos('octocat'))
      .rejects.toThrow('rate limit exceeded');
  });
});

Nock vs MSW

// Nock: intercepts at Node.js http module level
// - Only works in Node.js
// - No browser support
// - Simpler for pure Node.js testing

// MSW: intercepts at service worker / Node.js level
// - Works in both browser and Node.js
// - Same handlers for both environments
// - More complex setup

// When to use Nock:
// - Pure Node.js services, no browser testing needed
// - Already heavily invested in Nock patterns
// - Simpler integration for specific Node.js HTTP scenarios

Other Tools Worth Knowing

Playwright for API Testing

// playwright.config.ts — API testing without a browser
import { defineConfig } from '@playwright/test';
export default defineConfig({ testDir: './tests' });

// api.spec.ts
import { test, expect } from '@playwright/test';

test('users API returns 200', async ({ request }) => {
  const response = await request.get('http://localhost:3000/api/users');
  expect(response.ok()).toBeTruthy();
  const users = await response.json();
  expect(users).toBeInstanceOf(Array);
});

test('create user', async ({ request }) => {
  const response = await request.post('http://localhost:3000/api/users', {
    data: { name: 'Alice', email: 'alice@example.com' },
  });
  expect(response.status()).toBe(201);
  const user = await response.json();
  expect(user.id).toBeDefined();
});

Playwright's request context for API testing is excellent when you want to test against a running server with real HTTP.

Undici for Low-Level HTTP Assertions

import { MockAgent, setGlobalDispatcher } from 'undici';

const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);

const mockPool = mockAgent.get('https://api.example.com');
mockPool.intercept({ path: '/users', method: 'GET' })
  .reply(200, [{ id: 1, name: 'Alice' }]);

Undici is Node.js's built-in HTTP library. Its mock agent is useful for testing code that uses native fetch in Node.js 22+.

Choosing Your API Testing Tools

ToolTests your APIMocks external APIsBrowser support
SupertestYesNoNo
MSWPartialYesYes
NockNoYesNo
Playwright APIYesVia route.fulfillYes

Recommended combination for most projects:

// Integration: Supertest for your routes
// External APIs: MSW (same handlers in browser dev mode and Node.js tests)
// E2E: Playwright for real HTTP testing

// vitest.setup.ts
import { server } from './mocks/node'; // MSW
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// api.test.ts — Supertest for your routes
const response = await request(app).get('/api/users').expect(200);

// users.service.test.ts — MSW for external APIs
// Stripe API is mocked via MSW handlers automatically

Production Testing Strategies and CI Integration

Adopting API mocking tools effectively in production CI pipelines requires more than getting individual tests to pass. With MSW, you should enable onUnhandledRequest: 'error' in CI environments so that any API call your test code makes without a corresponding handler immediately fails the test suite — this prevents silent coverage gaps where new API calls go unmocked and tests pass only because the request silently falls through. In team settings, centralizing handler definitions in a shared src/mocks/handlers.ts file that both the browser dev server and the Node.js test setup import ensures that frontend component tests and backend service tests stay synchronized with the same mock contract. For Supertest specifically, spinning up your Express or Fastify app instance once per test file in a beforeAll block rather than per-test significantly cuts CI run times on large route suites. Teams using GitHub Actions or similar CI platforms should also consider caching the Playwright browser binaries between runs — browser downloads add over a minute to cold-start CI pipelines.

TypeScript Integration and Type Safety in Test Mocks

One underappreciated benefit of MSW v2 is that handlers are fully type-safe when you use the HttpResponse.json() helper — TypeScript can enforce that your mock response matches the shape your application code expects. Pair this with @types/supertest and typed route handlers, and you can catch response shape mismatches at compile time rather than at runtime in CI. For teams using tRPC or ts-rest, consider generating your MSW handlers directly from the contract types — this ensures your mock responses always match the currently defined API contract and prevents mocks from drifting as the real API evolves. Nock's TypeScript support is weaker by comparison; it does not validate response bodies against any schema at the type level, which means Nock-based tests are more vulnerable to mock drift over time. If your codebase is TypeScript-first and you are setting up HTTP mocking from scratch, MSW is clearly the right choice from a type-safety perspective.

Security Testing and Edge Case Coverage

API testing suites often cover the happy path but neglect security-relevant edge cases that only surface under realistic network conditions. MSW makes it straightforward to test your application's handling of malformed responses — returning a 200 with an unexpected JSON structure, for example — because you control the full response at the network intercept layer. Test that your application handles 401 and 403 responses gracefully, that expired tokens result in proper re-authentication flows, and that rate-limit responses (429 with a Retry-After header) trigger your backoff logic. For authentication-heavy Supertest suites, use supertest-session to maintain cookie-based session state across requests and verify that protected routes genuinely reject unauthenticated callers. Nock allows you to simulate network errors like ECONNREFUSED and ETIMEDOUT using nock.replyWithError(), which is valuable for testing your HTTP client's timeout and retry logic — scenarios that are nearly impossible to trigger reliably against real servers in a CI environment.

Migrating from Nock to MSW in Existing Codebases

Many Node.js projects that predate MSW have extensive Nock-based test suites. The migration path is gradual and low-risk: MSW and Nock can coexist in the same test suite during transition because they intercept at different layers. Start by setting up the MSW server in a new beforeAll/afterAll block alongside existing Nock calls, then migrate test files one by one, replacing nock('https://api.example.com').get('/path').reply(200, data) with http.get('https://api.example.com/path', () => HttpResponse.json(data)). The main behavioral difference to account for is that Nock intercepts are consumed once by default and must be re-registered for repeated calls, while MSW handlers persist until explicitly reset with server.resetHandlers(). Teams migrating also gain the significant benefit that the same MSW handlers work in Storybook stories and browser development mode — meaning the effort of defining good mocks in tests directly pays dividends in frontend development workflows.

Compare testing package downloads on PkgPulse.

See also: Best npm Packages for API Testing and Mocking 2026 and Best API Mocking Libraries for JavaScript Testing 2026, c8 vs nyc vs Istanbul: JavaScript Code Coverage in 2026.

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.