Skip to main content

Best npm Packages for API Testing and Mocking 2026

·PkgPulse Team
0

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)

Designing a Sustainable Mock Strategy

The most important architectural decision in API testing is deciding what to mock and at what boundary. Over-mocking leads to tests that pass while the actual integration is broken. Under-mocking creates flaky tests that fail when external services are slow or unavailable. MSW's design philosophy — mock at the network boundary rather than at the module boundary — produces tests that exercise the actual fetch call, response parsing, and error handling code rather than replacing them. For integration tests that test the API layer itself (Supertest), avoid mocking the database — use a test database seeded with known fixtures, and reset it between tests using transactions that are rolled back after each test case.

Production Considerations and Test Architecture

The strategic value of MSW v2 over nock is that it tests the same code path that runs in production browsers. Nock patches Node.js's http module, meaning a test can pass with nock even if the browser-compatible fetch call would fail in production. MSW intercepts at the fetch level in Node.js via @mswjs/interceptors, matching the actual HTTP client used in modern Next.js and Vite applications. For teams running Playwright end-to-end tests alongside unit tests, MSW handler definitions can be reused across both test levels — define handlers once, use them in Vitest component tests and Playwright network interception without duplicating mock logic.

Handler Organization and Maintenance

As a codebase grows, MSW handler files can become difficult to maintain if every test adds server overrides ad hoc. A maintainable pattern is to organize handlers by domain: handlers/users.ts, handlers/packages.ts, handlers/auth.ts, then compose them in a root handlers/index.ts. Each domain handler module exports both the default happy-path handlers and named error handlers for override scenarios — export const getUserError = http.get('/api/users', () => HttpResponse.json({error: 'Server error'}, {status: 500})). Tests import the specific error handler and use server.use(getUserError) rather than defining inline lambdas, which improves readability and enables reuse across test files that need the same failure scenario.

Testing Authentication Flows

Both MSW and Supertest require deliberate attention when testing authenticated routes. With MSW, the mock server does not enforce authentication by default — handlers respond regardless of whether the request contains a valid token. Add an MSW middleware handler that checks for Authorization headers and returns 401 for specific test cases where you need to verify auth error handling. With Supertest against a real Express app, use a test-token pattern where the auth middleware checks process.env.NODE_ENV === 'test' to accept a static bearer token, keeping tests hermetic without needing a running auth server. Never disable authentication entirely in tests — verifying that protected routes reject unauthenticated requests is one of the most valuable test categories.

Type Safety in API Tests

The Hono test client provides the strongest type safety story for API testing in 2026. When a Hono route is defined with a typed Zod validator, the test client's $post method types both the request body and the response body — TypeScript catches mismatches between what the test sends and what the handler expects before tests run. For Supertest, TypeScript types from @types/supertest cover the response object but not the request body shape. Consider generating types from your OpenAPI schema and using them to type Supertest request bodies — tools like openapi-typescript generate TypeScript types from OpenAPI specs that can be applied to both the implementation and the test code, ensuring they stay synchronized.

Choosing Based on Test Runner and Framework

The choice of API mocking library often depends on which test runner you use and which HTTP layer your application uses.

MSW with Vitest is the most common combination for React and full-stack TypeScript projects in 2026. MSW's setupServer() integrates with Vitest's lifecycle hooks (beforeAll, afterEach, afterAll) cleanly, and Vitest's native ESM support avoids the CommonJS compatibility issues that affect some MSW configurations with Jest. The @mswjs/interceptors package (MSW's underlying network interception layer) also works standalone for lower-level interception.

Supertest with Jest or Vitest remains the dominant pattern for Express and Fastify API route testing. Supertest's .get('/api/users').expect(200).expect('Content-Type', /json/) assertion chain reads like an integration test specification, and the chained expect API validates multiple aspects of the response in a single readable expression.

Nock for legacy codebases is still widely used in projects that predate MSW's widespread adoption. Nock intercepts at the http/https module level in Node.js, which means it works regardless of which HTTP client library is used. Teams with established Nock test suites rarely migrate since Nock is actively maintained and the migration ROI is low for working test suites.

Parallelizing API Tests for Speed

API test suites that make real HTTP calls or hit a test database become slow as the test count grows, making parallel execution important. Vitest's built-in parallel test execution runs test files concurrently in separate worker threads — each test file gets its own process, so MSW server setup and teardown in beforeAll/afterAll hooks don't interfere across files. The critical constraint is that parallel test files must not share mutable state in a shared database — either use separate test database schemas per worker (Vitest's workerIndex variable identifies each worker), or run each test in a database transaction that rolls back after the test completes. Supertest's supertest(app) creates a new HTTP server for each test file, which is safe to parallelize since each app instance binds to an ephemeral port. For test suites where parallelism causes flakiness due to port conflicts or shared resources, use Vitest's --pool-options.threads.minThreads=1 to limit concurrency while still benefiting from Vitest's other performance features like watch mode and test isolation.

Contract Testing and API Compatibility

Beyond unit and integration testing, contract testing verifies that the API producer and consumer agree on the API shape — catching breaking changes before they reach production. Pact is the most popular contract testing framework for JavaScript, and it integrates well with MSW for consumer-side contract test generation. When a consumer test runs with MSW handlers, Pact records the interactions (request + response pairs) into a pact file that the API producer runs against the real implementation to verify compatibility. This pattern is particularly valuable for microservice teams where the frontend and backend are developed independently. For simpler use cases, snapshot testing of API response shapes using Vitest's expect(response).toMatchSnapshot() provides a lightweight form of contract testing — the snapshot captures the response structure, and any future change that alters the shape fails the test, prompting a deliberate snapshot update rather than a silent regression.

Compare msw, supertest, and nock download trends on PkgPulse.

See also: Best npm Packages for API Testing and Mocking in 2026 and Best API Mocking Libraries for JavaScript Testing 2026, MSW vs Nock vs axios-mock-adapter.

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.