Best npm Packages for API Testing and Mocking in 2026
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
expectAPI 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:
- Testing your own API: Does
/api/usersreturn the right data? - 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.mutationhandlers - 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
| Tool | Tests your API | Mocks external APIs | Browser support |
|---|---|---|---|
| Supertest | Yes | No | No |
| MSW | Partial | Yes | Yes |
| Nock | No | Yes | No |
| Playwright API | Yes | Via route.fulfill | Yes |
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.
See the live comparison
View best npm packages api testing mocking on PkgPulse →