Skip to main content

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

PackageWeekly DownloadsTrend
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.

Comments

Stay Updated

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