Skip to main content

Best npm Packages for API Testing and Mocking in 2026

·PkgPulse Team

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

Compare testing package downloads on PkgPulse.

Comments

Stay Updated

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