axios-mock-adapter, fetch-mock, msw, and nock are essential tools for simulating network requests in JavaScript applications, but they operate at different layers of the stack. axios-mock-adapter intercepts requests specifically within the Axios library, making it ideal for projects tightly coupled to that client. fetch-mock replaces the native fetch API globally, offering a lightweight solution for testing code that relies on standard browser networking. msw (Mock Service Worker) takes a unique approach by intercepting requests at the network level using Service Workers, allowing the same mocks to work in both local development and automated tests without changing application code. nock is a veteran tool designed for Node.js environments that intercepts HTTP requests at the low-level socket layer, making it powerful for backend testing but unsuitable for direct browser usage.
Mocking network requests is a critical part of frontend development, but not all mocking tools are built the same. Some patch specific libraries, some replace global functions, and others intercept traffic at the network level. Choosing the wrong one can lock you into a specific HTTP client or make your tests brittle. Let's break down how axios-mock-adapter, fetch-mock, msw, and nock actually work and where they fit in your architecture.
The most important difference is where these tools step in to stop the request.
axios-mock-adapter hooks directly into Axios. It replaces the internal adapter that Axios uses to send requests. If you aren't using Axios, this tool is useless to you.
// axios-mock-adapter: Tied strictly to the Axios instance
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from 'axios';
const mock = new AxiosMockAdapter(axios);
mock.onGet('/users').reply(200, { data: ['Alice'] });
// This works because it goes through axios
await axios.get('/users');
fetch-mock replaces the global fetch function in your JavaScript environment. It doesn't care what library you use, as long as that library eventually calls fetch.
// fetch-mock: Replaces global fetch
import fetchMock from 'fetch-mock';
fetchMock.get('/users', { data: ['Alice'] });
// This works with native fetch or libraries wrapping fetch
await fetch('/users');
await myCustomFetchWrapper('/users');
msw (Mock Service Worker) operates at the network level using a Service Worker in the browser and a Node.js interceptor in tests. It sits between your app and the actual network, meaning your code thinks it's making a real request.
// msw: Intercepts at the network layer
import { setupWorker, rest } from 'msw';
const worker = setupWorker(
rest.get('/users', (req, res, ctx) => {
return res(ctx.json({ data: ['Alice'] }));
})
);
worker.start();
// Your code remains unchanged
await fetch('/users');
await axios.get('/users');
nock intercepts HTTP requests at the low-level socket layer in Node.js. It is invisible to your code but only exists in the Node runtime.
// nock: Intercepts Node HTTP sockets
import nock from 'nock';
nock('https://api.example.com')
.get('/users')
.reply(200, { data: ['Alice'] });
// Works in Node.js environments only
await fetch('https://api.example.com/users');
Some tools are strictly for running automated tests, while others can improve your daily coding workflow.
axios-mock-adapter and fetch-mock are typically used only in test suites. You usually wrap your test code to activate them. Using them during local development often requires manually injecting mock logic into your app, which clutters your source code.
// Typical test-only setup for axios-mock-adapter
describe('UserComponent', () => {
it('loads users', () => {
const mock = new AxiosMockAdapter(axios);
mock.onGet('/users').reply(200, []);
render(<UserComponent />);
// ... assertions
mock.restore();
});
});
msw shines here because it allows you to run the exact same mocks in your local browser while you develop. You don't need to change your app code to switch between "mock mode" and "real API mode." You just start the worker.
// msw: Same handlers for dev and test
// src/mocks/handlers.js
export const handlers = [
rest.get('/users', (req, res, ctx) => res(ctx.json([])))
];
// src/main.js (Development)
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./mocks/browser');
worker.start();
}
nock is strictly for Node.js testing. It cannot run in a browser, so it offers no help for local frontend development in the browser window.
// nock: Strictly for Node test environments
test('fetches users', async () => {
nock('https://api.example.com').get('/users').reply(200, []);
await runServerSideLogic();
});
Real APIs aren't just static JSON blobs. They have delays, errors, and logic based on request bodies.
axios-mock-adapter lets you pass a function to reply to handle dynamic logic, but it is limited to the scope of Axios.
// axios-mock-adapter: Dynamic reply
mock.onPost('/login').reply((config) => {
const { password } = JSON.parse(config.data);
if (password === 'secret') return [200, { token: '123' }];
return [401, { error: 'Invalid' }];
});
fetch-mock also supports dynamic functions and has built-in helpers for simulating network delays easily.
// fetch-mock: Dynamic reply with delay
fetchMock.post('/login', (url, opts) => {
const body = JSON.parse(opts.body);
return body.password === 'secret' ? { token: '123' } : 401;
}, { delay: 500 }); // Simulates 500ms latency
msw provides a very expressive context API (ctx) to set headers, cookies, and status codes dynamically, closely mimicking a real server response.
// msw: Rich context for dynamic responses
rest.post('/login', (req, res, ctx) => {
const { password } = req.body;
if (password === 'secret') {
return res(ctx.delay(500), ctx.setCookie('auth', '123'), ctx.json({ token: '123' }));
}
return res(ctx.status(401), ctx.json({ error: 'Invalid' }));
});
nock is extremely powerful for complex scenarios like reply chaining or verifying that a request happened a specific number of times, which is vital for backend integration tests.
// nock: Advanced chaining and verification
nock('https://api.example.com')
.post('/login')
.times(2) // Expect exactly 2 calls
.reply(200, { token: '123' })
.post('/login')
.reply(429, { error: 'Too Many Requests' });
It is crucial to note the current status of these libraries before adopting them.
axios-mock-adapter: Actively maintained. Safe to use for Axios projects.fetch-mock: The original fetch-mock package is in maintenance mode. The maintainers recommend migrating to @fetch-mock/fetch-mock (the scoped version) or considering MSW for new projects. For this comparison, we refer to the capabilities of the ecosystem, but be cautious with the legacy package name.msw: Highly active and becoming the industry standard for frontend mocking. Strongly recommended for new projects.nock: The gold standard for Node.js. Actively maintained and essential for backend testing.Note: While
fetch-mockworks, the ecosystem is shifting towardsmswfor frontend work becausemswdoesn't require you to wrap your fetch calls or worry about global state leakage in complex test suites.
Where can you actually run these?
| Package | Browser (Dev) | Browser (Test) | Node.js (Test) | Node.js (Dev) |
|---|---|---|---|---|
axios-mock-adapter | β (Manual only) | β | β | β |
fetch-mock | β (Manual only) | β | β | β |
msw | β (Native) | β | β | β |
nock | β | β | β | β |
You have a large codebase deeply tied to Axios, and you just need to write unit tests for components quickly.
axios-mock-adapterYou are building a new application and want to develop features against mocked APIs before the backend is ready.
mswYou are writing tests for a Next.js API route or a Express server that calls external third-party APIs.
nockmsw can do Node interception now, but nock has decades of edge-case handling for Node HTTP streams.You have a simple Node script that uses fetch and you don't want to set up a Service Worker.
fetch-mock (or native MockAgent in Node 15+)msw is overkill for a 50-line script. fetch-mock provides a quick global override without the boilerplate of setting up a worker instance.If you are building a modern frontend application, msw is generally the best architectural choice. It treats mocking as a network concern rather than a library concern, which frees you to change your HTTP client (from Axios to Fetch to TanStack Query) without breaking your tests.
Stick with axios-mock-adapter only if you are maintaining an older Axios-heavy codebase and need a quick fix for unit tests. Use nock when you are testing server-side Node.js code where low-level HTTP control is required. Avoid relying on global patches like fetch-mock for complex applications, as they can sometimes hide bugs related to how different libraries handle the global fetch polyfill.
Choose axios-mock-adapter if your project exclusively uses Axios for HTTP requests and you need a quick, zero-configuration way to mock responses within unit tests. It is the most straightforward option for isolating components that depend on Axios without setting up complex network interception layers. However, avoid it if you plan to migrate away from Axios or if you need mocks to function in a real browser environment during development, as it only works where the Axios adapter runs.
Select fetch-mock when your codebase relies on the native fetch API or a lightweight wrapper, and you need to simulate network conditions directly in your test environment. It is excellent for CI pipelines where installing a Service Worker might be overkill or problematic. Be aware that it modifies the global fetch function, which can sometimes lead to side effects if not properly cleaned up between tests, and it does not provide the same 'real network' experience as MSW for local development.
Adopt msw if you want a unified mocking strategy that works identically in your local browser, integration tests, and end-to-end tests. It is the superior choice for teams that want to develop against realistic API contracts without changing a single line of application code, as it intercepts requests at the network level rather than patching libraries. While it has a slightly steeper learning curve due to the Service Worker concept, the ability to share mock definitions between development and testing makes it the most robust long-term architectural decision.
Use nock primarily for testing Node.js backend services, server-side rendering logic, or build scripts where you need to intercept low-level HTTP calls. It is the industry standard for verifying that your server code correctly handles external API responses, headers, and retry logic in a Node environment. Do not choose nock for client-side browser testing, as it relies on Node's internal HTTP modules and cannot intercept requests made by browsers or Service Workers.
Axios adapter that allows to easily mock requests
Using npm:
$ npm install axios-mock-adapter --save-dev
It's also available as a UMD build:
axios-mock-adapter works on Node as well as in a browser, it works with axios v0.17.0 and above.
Mocking a GET request
const axios = require("axios");
const AxiosMockAdapter = require("axios-mock-adapter");
// This sets the mock adapter on the default instance
const mock = new AxiosMockAdapter(axios);
// Mock any GET request to /users
// arguments for reply are (status, data, headers)
mock.onGet("/users").reply(200, {
users: [{ id: 1, name: "John Smith" }],
});
axios.get("/users").then(function (response) {
console.log(response.data);
});
Mocking a GET request with specific parameters
const axios = require("axios");
const AxiosMockAdapter = require("axios-mock-adapter");
// This sets the mock adapter on the default instance
const mock = new AxiosMockAdapter(axios);
// Mock GET request to /users when param `searchText` is 'John'
// arguments for reply are (status, data, headers)
mock.onGet("/users", { params: { searchText: "John" } }).reply(200, {
users: [{ id: 1, name: "John Smith" }],
});
axios
.get("/users", { params: { searchText: "John" } })
.then(function (response) {
console.log(response.data);
});
When using params, you must match all key/value pairs passed to that option.
To add a delay to responses, specify a delay amount (in milliseconds) when instantiating the adapter
// All requests using this instance will have a 2 seconds delay:
const mock = new AxiosMockAdapter(axiosInstance, { delayResponse: 2000 });
You can restore the original adapter (which will remove the mocking behavior)
mock.restore();
You can also reset the registered mock handlers with resetHandlers
mock.resetHandlers();
You can reset both registered mock handlers and history items with reset
mock.reset();
reset is different from restore in that restore removes the mocking from the axios instance completely,
whereas reset only removes all mock handlers that were added with onGet, onPost, etc. but leaves the mocking in place.
Mock a low level network error
// Returns a failed promise with Error('Network Error');
mock.onGet("/users").networkError();
// networkErrorOnce can be used to mock a network error only once
mock.onGet("/users").networkErrorOnce();
Mock a network timeout
// Returns a failed promise with Error with code set to 'ECONNABORTED'
mock.onGet("/users").timeout();
// timeoutOnce can be used to mock a timeout only once
mock.onGet("/users").timeoutOnce();
Passing a function to reply
mock.onGet("/users").reply(function (config) {
// `config` is the axios config and contains things like the url
// return an array in the form of [status, data, headers]
return [
200,
{
users: [{ id: 1, name: "John Smith" }],
},
];
});
Passing a function to reply that returns an axios request, essentially mocking a redirect
mock.onPost("/foo").reply(function (config) {
return axios.get("/bar");
});
Using a regex
mock.onGet(/\/users\/\d+/).reply(function (config) {
// the actual id can be grabbed from config.url
return [200, {}];
});
Using variables in regex
const usersUri = "/users";
const url = new RegExp(`${usersUri}/*`);
mock.onGet(url).reply(200, users);
Specify no path to match by verb alone
// Reject all POST requests with HTTP 500
mock.onPost().reply(500);
Chaining is also supported
mock.onGet("/users").reply(200, users).onGet("/posts").reply(200, posts);
.replyOnce() can be used to let the mock only reply once
mock
.onGet("/users")
.replyOnce(200, users) // After the first request to /users, this handler is removed
.onGet("/users")
.replyOnce(500); // The second request to /users will have status code 500
// Any following request would return a 404 since there are
// no matching handlers left
Mocking any request to a given url
// mocks GET, POST, ... requests to /foo
mock.onAny("/foo").reply(200);
.onAny can be useful when you want to test for a specific order of requests
// Expected order of requests:
const responses = [
["GET", "/foo", 200, { foo: "bar" }],
["POST", "/bar", 200],
["PUT", "/baz", 200],
];
// Match ALL requests
mock.onAny().reply((config) => {
const [method, url, ...response] = responses.shift();
if (config.url === url && config.method.toUpperCase() === method)
return response;
// Unexpected request, error out
return [500, {}];
});
Requests that do not map to a mock handler are rejected with a HTTP 404 response. Since
handlers are matched in order, a final onAny() can be used to change the default
behaviour
// Mock GET requests to /foo, reject all others with HTTP 500
mock.onGet("/foo").reply(200).onAny().reply(500);
Mocking a request with a specific request body/data
mock.onPut("/product", { id: 4, name: "foo" }).reply(204);
Using an asymmetric matcher, for example Jest matchers
mock
.onPost(
"/product",
{ id: 1 },
{
headers: expect.objectContaining({
Authorization: expect.stringMatching(/^Basic /),
})
}
)
.reply(204);
Using a custom asymmetric matcher (any object that has a asymmetricMatch property)
mock
.onPost("/product", {
asymmetricMatch: function (actual) {
return ["computer", "phone"].includes(actual["type"]);
},
})
.reply(204);
.passThrough() forwards the matched request over network
// Mock POST requests to /api with HTTP 201, but forward
// GET requests to server
mock
.onPost(/^\/api/)
.reply(201)
.onGet(/^\/api/)
.passThrough();
Recall that the order of handlers is significant
// Mock specific requests, but let unmatched ones through
mock
.onGet("/foo")
.reply(200)
.onPut("/bar", { xyz: "abc" })
.reply(204)
.onAny()
.passThrough();
Note that passThrough requests are not subject to delaying by delayResponse.
If you set onNoMatch option to passthrough all requests would be forwarded over network by default
// Mock all requests to /foo with HTTP 200, but forward
// any others requests to server
const mock = new AxiosMockAdapter(axiosInstance, { onNoMatch: "passthrough" });
mock.onAny("/foo").reply(200);
Using onNoMatch option with throwException to throw an exception when a request is made without match any handler. It's helpful to debug your test mocks.
const mock = new AxiosMockAdapter(axiosInstance, { onNoMatch: "throwException" });
mock.onAny("/foo").reply(200);
axios.get("/unexistent-path");
// Exception message on console:
//
// Could not find mock for:
// {
// "method": "get",
// "url": "http://localhost/unexistent-path"
// }
As of 1.7.0, reply function may return a Promise:
mock.onGet("/product").reply(function (config) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
if (Math.random() > 0.1) {
resolve([200, { id: 4, name: "foo" }]);
} else {
// reject() reason will be passed as-is.
// Use HTTP error status code to simulate server failure.
resolve([500, { success: false }]);
}
}, 1000);
});
});
Composing from multiple sources with Promises:
const normalAxios = axios.create();
const mockAxios = axios.create();
const mock = new AxiosMockAdapter(mockAxios);
mock
.onGet("/orders")
.reply(() =>
Promise.all([
normalAxios.get("/api/v1/orders").then((resp) => resp.data),
normalAxios.get("/api/v2/orders").then((resp) => resp.data),
{ id: "-1", content: "extra row 1" },
{ id: "-2", content: "extra row 2" },
]).then((sources) => [
200,
sources.reduce((agg, source) => agg.concat(source)),
])
);
The history property allows you to enumerate existing axios request objects. The property is an object of verb keys referencing arrays of request objects.
This is useful for testing.
describe("Feature", () => {
it("requests an endpoint", (done) => {
const mock = new AxiosMockAdapter(axios);
mock.onPost("/endpoint").replyOnce(200);
feature
.request()
.then(() => {
expect(mock.history.post.length).toBe(1);
expect(mock.history.post[0].data).toBe(JSON.stringify({ foo: "bar" }));
})
.then(done)
.catch(done.fail);
});
});
You can clear the history with resetHistory
mock.resetHistory();