Testing Axios API Abstractions with MSW

Testing Axios API abstractions with Mock Service Worker (MSW) allows you to validate your application's data fetching, request configuration, and error handling without hitting real network endpoints. This guide demonstrates how to configure MSW for a Node-based testing environment (such as Vitest or Jest), create interceptor handlers, and execute reliable tests for your custom Axios client and service layers.

1. Define the Axios API Abstraction

Create an abstracted API client using an Axios instance. This layer typically configures base URLs, default headers, and custom response transformations.

// src/api/userClient.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  headers: {
    'Content-Type': 'application/json',
  },
});

export interface User {
  id: string;
  name: string;
}

export const getUserById = async (id: string): Promise<User> => {
  const response = await apiClient.get<User>(`/users/${id}`);
  return response.data;
};

export const createUser = async (name: string): Promise<User> => {
  const response = await apiClient.post<User>('/users', { name });
  return response.data;
};

2. Set Up the MSW Node Server

Install MSW (npm install msw --save-dev) and configure the server instance to intercept HTTP requests during testing.

// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://api.example.com/users/:id', ({ params }) => {
    const { id } = params;

    if (id === '1') {
      return HttpResponse.json({ id: '1', name: 'Jane Doe' }, { status: 200 });
    }

    return new HttpResponse(null, { status: 404 });
  }),

  http.post('https://api.example.com/users', async ({ request }) => {
    const body = (await request.json()) as { name: string };
    return HttpResponse.json(
      { id: '2', name: body.name },
      { status: 201 }
    );
  }),
];

export const server = setupServer(...handlers);

3. Configure Test Lifecycle Hooks

Configure your test runner to start the server before any tests run, reset handlers between tests to prevent test pollution, and shut down the server when finished.

// src/api/userClient.test.ts
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';
import { getUserById, createUser } from './userClient';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

4. Write Tests for API Functions

Testing Successful Requests

Test that your abstraction correctly unwraps responses and parses the returned data:

describe('userClient API Abstraction', () => {
  it('fetches a user by ID successfully', async () => {
    const user = await getUserById('1');

    expect(user).toEqual({
      id: '1',
      name: 'Jane Doe',
    });
  });

  it('creates a new user successfully', async () => {
    const newUser = await createUser('John Smith');

    expect(newUser).toEqual({
      id: '2',
      name: 'John Smith',
    });
  });
});

Testing Error Responses

Use runtime overrides via server.use() to simulate API failures and verify that your Axios abstraction handles errors as expected.

  it('throws an error when user is not found (404)', async () => {
    await expect(getUserById('999')).rejects.toThrow();
  });

  it('handles server errors (500)', async () => {
    server.use(
      http.get('https://api.example.com/users/:id', () => {
        return new HttpResponse(null, { status: 500 });
      })
    );

    await expect(getUserById('1')).rejects.toThrow();
  });

Testing Interceptors and Custom Headers

If your abstraction injects tokens or processes error responses via interceptors, verify that the interceptor sends the correct headers:

  it('attaches correct headers to outgoing requests', async () => {
    let capturedAuthHeader: string | null = null;

    server.use(
      http.get('https://api.example.com/users/:id', ({ request }) => {
        capturedAuthHeader = request.headers.get('Content-Type');
        return HttpResponse.json({ id: '1', name: 'Jane Doe' });
      })
    );

    await getUserById('1');
    expect(capturedAuthHeader).toBe('application/json');
  });