Best npm Packages for API Testing and Mocking 2026
·PkgPulse Team
TL;DR
MSW v2 (Mock Service Worker) is the 2026 standard for API mocking in React/Next.js tests. It works in both browser (service worker) and Node.js (interceptor) — the same mock handlers work in Vitest, Jest, Playwright, and Storybook. Supertest is still the go-to for testing Express/Hono API routes directly. Nock is legacy but still widely used. For Next.js App Router: Hono test client or node:test with fetch are the new patterns.
Key Takeaways
- MSW v2: HTTP interception in Node.js + browser, same handlers everywhere
- Supertest: HTTP assertions for Express/Fastify/Hono, no server startup needed
- Nock: HTTP mocking for Node.js, still 5M downloads/week but legacy vs MSW
- Hono test client: Type-safe API testing for Hono routes,
hono/testing - Vitest + MSW: The combination for modern React apps with API calls
- 2026 trend: MSW v2 unifying mocking across test environments
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
msw | ~5M | ↑ Fast growing |
nock | ~5M | → Stable (legacy) |
supertest | ~7M | → Stable |
@mswjs/data | ~200K | ↑ Growing |
MSW v2: The Modern Standard
npm install msw --save-dev
npx msw init public/ --save # For browser (Next.js/Vite)
// src/mocks/handlers.ts — define mock handlers:
import { http, HttpResponse } from 'msw';
export const handlers = [
// Mock a GET endpoint:
http.get('/api/users', ({ request }) => {
const url = new URL(request.url);
const plan = url.searchParams.get('plan');
const users = [
{ id: '1', email: 'alice@example.com', plan: 'pro' },
{ id: '2', email: 'bob@example.com', plan: 'free' },
].filter(u => !plan || u.plan === plan);
return HttpResponse.json(users);
}),
// Mock a POST with body:
http.post('/api/users', async ({ request }) => {
const body = await request.json();
if (!body.email) {
return HttpResponse.json({ error: 'Email required' }, { status: 400 });
}
return HttpResponse.json(
{ id: crypto.randomUUID(), ...body },
{ status: 201 }
);
}),
// Mock with delay (simulate slow API):
http.get('/api/analytics', async () => {
await new Promise(resolve => setTimeout(resolve, 500));
return HttpResponse.json({ views: 12500, clicks: 342 });
}),
// Mock network error:
http.get('/api/external-service', () => {
return HttpResponse.error(); // Network error
}),
];
// src/mocks/server.ts — Node.js test environment:
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// vitest.setup.ts — global setup:
import { beforeAll, afterAll, afterEach } from 'vitest';
import { server } from './src/mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Component test with MSW:
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from './UserList';
import { server } from '@/mocks/server';
import { http, HttpResponse } from 'msw';
test('renders users from API', async () => {
render(<UserList />);
// Wait for the API to load:
await waitFor(() => {
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
});
test('handles API error gracefully', async () => {
// Override handler for this test:
server.use(
http.get('/api/users', () => HttpResponse.json({ error: 'Server error' }, { status: 500 }))
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Failed to load users')).toBeInTheDocument();
});
});
test('shows loading state', async () => {
server.use(
http.get('/api/users', async () => {
await new Promise(r => setTimeout(r, 1000)); // Slow response
return HttpResponse.json([]);
})
);
render(<UserList />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
Supertest: Route Testing
npm install supertest --save-dev
npm install @types/supertest --save-dev
// Testing Hono routes with Supertest:
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from '../src/app'; // Your Hono/Express app
describe('POST /api/users', () => {
it('creates a user', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', 'Bearer test-token')
.send({ email: 'test@example.com', name: 'Test User' })
.expect(201);
expect(res.body).toMatchObject({
email: 'test@example.com',
name: 'Test User',
});
expect(res.body.id).toBeDefined();
});
it('validates required fields', async () => {
const res = await request(app)
.post('/api/users')
.send({}) // Missing email
.expect(400);
expect(res.body.error).toBe('Email required');
});
it('rejects unauthenticated requests', async () => {
await request(app)
.post('/api/users')
.send({ email: 'test@example.com' })
.expect(401);
});
});
describe('GET /api/users', () => {
it('returns users list', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', 'Bearer admin-token')
.expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
it('filters by plan', async () => {
const res = await request(app)
.get('/api/users?plan=pro')
.set('Authorization', 'Bearer admin-token')
.expect(200);
res.body.forEach(user => {
expect(user.plan).toBe('pro');
});
});
});
Hono Test Client: Type-Safe
// Hono has a built-in test client (no HTTP, in-process):
import { testClient } from 'hono/testing';
import { app } from '../src/app'; // Hono app
const client = testClient(app);
test('creates user', async () => {
const res = await client.api.users.$post({
json: { email: 'test@example.com', name: 'Test' },
header: { Authorization: 'Bearer token' },
});
// Type-safe response:
const body = await res.json();
expect(res.status).toBe(201);
expect(body.email).toBe('test@example.com');
});
@mswjs/data: Mock Database
// @mswjs/data — in-memory database for tests:
import { factory, primaryKey, oneOf, manyOf } from '@mswjs/data';
export const db = factory({
user: {
id: primaryKey(String),
email: String,
name: String,
plan: String,
},
post: {
id: primaryKey(String),
title: String,
author: oneOf('user'), // Relation
},
});
// MSW handlers that use the db:
export const handlers = [
http.get('/api/users', () => {
const users = db.user.getAll();
return HttpResponse.json(users);
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
const user = db.user.create({ id: crypto.randomUUID(), ...body });
return HttpResponse.json(user, { status: 201 });
}),
];
// In tests — seed and assert:
test('user list shows created users', async () => {
db.user.create({ id: '1', email: 'test@example.com', name: 'Test', plan: 'pro' });
const res = await fetch('/api/users');
const users = await res.json();
expect(users).toHaveLength(1);
expect(users[0].email).toBe('test@example.com');
db.user.delete({ where: { id: { equals: '1' } } }); // Cleanup
});
Decision Guide
Use MSW if:
→ Testing React components that make API calls
→ Want same mocks in unit tests AND Storybook AND browser
→ Need browser-compatible API mocking
→ New project — it's the 2026 standard
Use Supertest if:
→ Testing Express/Fastify/Hono route handlers directly
→ Integration tests for your API layer
→ Need to test middleware, auth, validation at HTTP level
Use Hono test client if:
→ Hono framework specifically
→ Want type-safe request/response in tests
→ No HTTP overhead (in-process)
Use Nock if:
→ Legacy codebase already using it
→ Node.js-only testing (no browser mocking needed)
→ Not migrating to MSW (both 5M/week, MSW growing faster)
Compare msw, supertest, and nock download trends on PkgPulse.