@luxass/msw-utils
v0.7.1
Published
Utilities for working with MSW
Readme
@luxass/msw-utils
A collection of utilities for working with MSW (Mock Service Worker).
[!IMPORTANT] This package is still in a very early stage of development and many things are still missing. If you have any suggestions or ideas, please open an issue or a pull request.
Installation
npm install @luxass/msw-utils mswRequires Node.js >=24.11.0 and MSW >=2.11.0 <3.0.0 || >=3.0.0 <4.0.0.
Requires TypeScript >=5.4 when using TypeScript. MSW v3 requires TypeScript >=5.9.
Usage
[!NOTE] This library requires MSW to be set up in your testing environment. Follow the MSW Vitest integration guide for complete setup instructions.
// test/utils.ts
import { createMockFetch } from "@luxass/msw-utils";
import { setupServer } from "msw/node";
export const server = setupServer();
export const mockFetch = createMockFetch({ mswServer: server });// vitest.setup.ts
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./test/utils.js";
// MSW v3
beforeAll(() => server.listen({ onUnhandledFrame: "error" }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());For MSW v2, replace the beforeAll call with:
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));// your-test.test.ts
import { HttpResponse } from "msw";
import { mockFetch } from "./test/utils.js";
test("should fetch version", async () => {
mockFetch("GET", "/api/v1/versions", () => {
return HttpResponse.json({ version: "1.0.0" });
});
const response = await fetch("/api/v1/versions");
const data = await response.json();
expect(data).toEqual({ version: "1.0.0" });
});If you prefer to avoid importing mockFetch in every test file, you can make it globally available:
// test/utils.ts
import { createMockFetch } from "@luxass/msw-utils";
import { setupServer } from "msw/node";
export const server = setupServer();
export const mockFetch = createMockFetch({ mswServer: server });// vitest.setup.ts
import { afterAll, afterEach, beforeAll, vi } from "vitest";
import { mockFetch, server } from "./test/utils.js";
vi.stubGlobal("mockFetch", mockFetch);
// MSW v3. With MSW v2, use onUnhandledRequest instead of onUnhandledFrame.
beforeAll(() => server.listen({ onUnhandledFrame: "error" }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());// your-test.test.ts
import { HttpResponse } from "msw";
// mockFetch is available globally via vi.stubGlobal
test("should fetch version", async () => {
mockFetch("GET", "/api/v1/versions", () => {
return HttpResponse.json({ version: "1.0.0" });
});
const response = await fetch("/api/v1/versions");
const data = await response.json();
expect(data).toEqual({ version: "1.0.0" });
});Typed Mock Fetch
The library provides type-safe mocking with TypeScript through two approaches. Registered URL/method pairs constrain response bodies. Unregistered pairs keep MSW's default response body type.
For inferred JSON and text responses, import HttpResponse from this package:
import { HttpResponse } from "@luxass/msw-utils";This export is the original MSW constructor with improved type signatures. It preserves MSW's runtime behavior, constructor, and other response helpers. It does not add a wrapper or runtime schema validation.
Global Type Registry (Module Augmentation)
You can augment the URLRegistry interface to define type-safe routes globally:
// types/msw-utils.d.ts
import "@luxass/msw-utils";
declare module "@luxass/msw-utils" {
interface URLRegistry {
"/api/users": {
GET: Array<{ id: number; name: string }>;
POST: { id: number; created: boolean };
};
"/api/posts/:id": {
GET: { id: string; title: string; body: string };
PUT: { id: string; updated: boolean };
};
}
}// your-test.test.ts
import { HttpResponse } from "@luxass/msw-utils";
import { mockFetch } from "./test/utils.js";
test("should fetch users with type safety", async () => {
// TypeScript enforces the response type matches URLRegistry
mockFetch("GET", "/api/users", () => {
return HttpResponse.json([
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
]);
});
const response = await fetch("/api/users");
const data = await response.json();
expect(data).toEqual([
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
]);
});
test("should create user with type safety", async () => {
mockFetch("POST", "/api/users", () => {
return HttpResponse.json({ id: 1, created: true });
});
const response = await fetch("/api/users", {
method: "POST",
body: JSON.stringify({ name: "John" }),
});
const data = await response.json();
expect(data).toEqual({ id: 1, created: true });
});Per-Instance Type Registry
Alternatively, you can use createTypedMockFetch to create a typed wrapper without global augmentation:
// test/utils.ts
import { createMockFetch, createTypedMockFetch } from "@luxass/msw-utils";
import { setupServer } from "msw/node";
interface MyURLs {
"/api/products": {
GET: Array<{ id: string; name: string; price: number }>;
POST: { id: string; created: boolean };
};
"/api/orders/:orderId": {
GET: { orderId: string; total: number; items: string[] };
};
}
export const server = setupServer();
export const mockFetch = createMockFetch({ mswServer: server });
export const typedMock = createTypedMockFetch<MyURLs>(mockFetch);// your-test.test.ts
import { HttpResponse } from "@luxass/msw-utils";
import { typedMock } from "./test/utils.js";
test("should fetch products with type safety", async () => {
// TypeScript enforces the response type matches MyURLs
typedMock("GET", "/api/products", () => {
return HttpResponse.json([
{ id: "1", name: "Widget", price: 9.99 },
{ id: "2", name: "Gadget", price: 19.99 },
]);
});
const response = await fetch("/api/products");
const data = await response.json();
expect(data).toHaveLength(2);
});
test("should handle path parameters with type safety", async () => {
typedMock("GET", "/api/orders/:orderId", ({ params }) => {
return HttpResponse.json({
orderId: params.orderId as string,
total: 99.99,
items: ["item1", "item2"],
});
});
const response = await fetch("/api/orders/123");
const data = await response.json();
expect(data.orderId).toBe("123");
});Typed batches infer each response body without explicit response type arguments:
typedMock([
["GET", "/api/products", () => HttpResponse.json([{ id: "1", name: "Widget", price: 9.99 }])],
["POST", "/api/products", () => HttpResponse.json({ id: "1", created: true })],
]);TypeScript rejects a response that does not match that entry's registered URL and method. Inference also preserves literal fields, nested arrays, tuples, and union responses.
You can still import HttpResponse directly from msw. That import retains MSW's original type signatures, which can require explicit response types in batches.
Both approaches support:
- Single method endpoints
- Multiple methods on the same URL
- Path parameters
- Batch registration of endpoints
API
createMockFetch(options)
Creates a mockFetch function that uses MSW server under the hood but provides a nicer developer experience.
Parameters
options.mswServer: An MSW server instance fromsetupServer()
Returns
The mockFetch function.
mockFetch(methods, url, resolver)
Register a single endpoint with one or more HTTP methods.
Parameters
methods: HTTP method(s) - can be a single method or array of methodsurl: The endpoint URL patternresolver: MSW HttpResponseResolver function
mockFetch(endpoints)
Register multiple endpoints at once.
Parameters
endpoints: Array of[methods, url, resolver]tuples
createTypedMockFetch(mockFetch)
Creates a type-safe wrapper around mockFetch with inline URL registry. Provides an alternative to module augmentation for users who prefer explicit type parameters.
Parameters
mockFetch: AMockFetchFninstance fromcreateMockFetch()
Type Parameters
Registry: Record mapping URLs to HTTP methods and their response types
Returns
A type-safe mockFetch wrapper with enforced response types based on the registry.
Example
interface MyURLs {
"/api/users": {
GET: User[];
POST: CreateUserResponse;
};
}
const mockFetch = createMockFetch({ mswServer: server });
const typedMock = createTypedMockFetch<MyURLs>(mockFetch);
// Type-safe - enforces User[] response
typedMock("GET", "/api/users", () => HttpResponse.json([...yourResponse]));
// Type error - wrong response type
typedMock("GET", "/api/users", () => HttpResponse.json("wrong"));HttpResponse
The MSW constructor with JSON and text body inference. Import it from @luxass/msw-utils to use typed batches without manual response type arguments.
new HttpResponse(...), explicit type arguments, and MSW's other static response helpers remain available. Responses are still MSW instances.
MSW version differences
mockFetch uses the installed MSW version. It does not change URL matching or resolver values to emulate another version.
Plain named parameters such as /users/:id work on both majors. Set replaceOpenAPIPathParams: true to convert /users/{id} to /users/:id.
MSW v3 replaces its URL matcher. Two differences affect string patterns:
- Repeated parameters such as
/files/:parts+and/files/:parts*return arrays in v2 and slash-separated strings in v3. For/files/a/b,params.partsis["a", "b"]in v2 and"a/b"in v3. - Inline regex constraints such as
/users/:id(\d+)match/users/123in v2 but not in v3. Plain/users/:idstill matches in both. This utility does not remove numeric constraints or acceptRegExpURL arguments.
MSW v3 does not manage a cookie store in Node.js. Use the cookies resolver argument for cookie-based logic, not request.headers.get("cookie").
See the MSW v3 migration guide for other upstream changes.
Testing
The shared integration suite runs against MSW 2.11.0, 2.15.0, 3.0.0, and 3.0.3 in isolated Vitest projects. Runtime-guard unit tests run once.
pnpm test
pnpm test --project msw-2
pnpm test --project msw-3
pnpm test --project msw-2-min --project msw-3-minRuntime Guards
The package also exports runtime type guards for MSW-specific types. Import from @luxass/msw-utils/runtime-guards:
isMSWError(error)
Checks if an error is an MSW internal error. MSW throws internal errors with the name "InternalError" and prefixes error messages with "[MSW]".
With MSW v3 in Node.js, a blocked fetch() rejects with a TypeError whose cause is the MSW error. Pass that cause to isMSWError. The Fetch wrapper itself is not an MSW error.
Parameters
error: The error to check (type:unknown)
Returns
true if the error is from MSW (type predicate: error is Error)
Example
import { isMSWError } from "@luxass/msw-utils/runtime-guards";
try {
// some code that might throw MSW errors
} catch (error) {
if (isMSWError(error)) {
console.log("This is an MSW error:", error.message);
} else {
console.log("This is not an MSW error");
}
}📄 License
Published under MIT License.
