API Mocking with MSW skill for Codex
Mock the network, not your fetch client. Codex mocks APIs at the network layer with MSW so tests run the real fetch or axios code, headers and error handling. It sets up shared typed handlers for tests, Storybook and local dev, fails on any unhandled request, overrides single responses per test for errors, latency and retries, and knows when respx, responses, nock or WireMock fit better.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- api-mocking-msw Installs to ~/.agents/skills/api-mocking-msw/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install api-mocking-msw The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project api-mocking-msw Installs to .agents/skills/api-mocking-msw/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/api-mocking-msw.zipunzip -o api-mocking-msw.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) api-mocking-msw PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $api-mocking-msw, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Mocks HTTP and GraphQL at the network boundary with Mock Service Worker (MSW 2.x http and graphql handlers, HttpResponse, setupServer in Node, setupWorker in the browser) and picks equivalents elsewhere (nock, respx, responses, WireMock) - shared handlers, per-test overrides with server.use, strict unhandled-request errors, error and latency scenarios, and typed fixtures that stay in sync with the real API. Use when tests call real APIs, stub fetch or axios by hand, when a frontend needs a mocked backend in Storybook or local dev, or when upgrading MSW 1 rest handlers to MSW 2.
Use it when
- Frontend or Node tests stub fetch or axios by hand
- Tests accidentally hit real APIs or hang without network
- Storybook or local dev needs a mocked backend
- Upgrading from MSW 1 to MSW 2
Use something else when
- You need proof the mock matches the real provider: use contract-testing
- You are testing an HTTP API you own end to end: use api-testing
How to check the result
Removing a handler makes the test fail with an unhandled request error, and the suite passes with the network disabled.
The mistake it prevents
Leaving onUnhandledRequest at its default warning, so a missing handler quietly hits the network or hangs instead of failing the test.
Mock at the network, not the client: tests then exercise the real fetch or axios code, headers, serialisation and error handling. A good setup fails loudly on any unmocked request, keeps one set of happy-path handlers shared by tests, Storybook and local dev, and lets each test override only the response it is about.
Inspect first
- Read
AGENTS.mdandpackage.json: MSW version (msw@^2assumed below), test runner (Vitest, Jest), environment (jsdom, happy-dom, node), Storybook presence. - Find existing mocks:
rg -n "msw|setupServer|jest.mock\(.*(axios|fetch)|vi.mock\(|nock\(" -g '!node_modules'. - Find the API contract: OpenAPI spec, generated client types, GraphQL schema. Mocks typed against it catch drift; hand-written shapes rot.
- Installing MSW needs network; MSW 2 needs Node 18+.
When MSW is the right tool
| Situation | Use |
|---|---|
| Frontend or Node code using fetch, axios, ky, Apollo, urql | MSW |
Python with requests | responses |
Python with httpx | respx |
Node legacy http module code, or MSW cannot be added | nock |
| JVM services, or language-agnostic stub server in Docker | WireMock |
| Verifying the mock matches the provider | Contract tests (contract-testing), not more mocks |
| You own the backend and can run it | Integration test against the real service (integration-testing) |
Do not mock your own module boundaries with MSW to avoid writing a unit test seam. MSW is for HTTP you do not control in that test.
Layout
src/mocks/
├── handlers.ts # happy-path handlers, one per endpoint
├── data.ts # typed fixture builders (or factories)
├── node.ts # setupServer(...handlers) for tests and Node
└── browser.ts # setupWorker(...handlers) for dev and Storybook
// src/mocks/handlers.ts
import { http, HttpResponse, graphql, delay } from "msw";
import type { components } from "../api/schema"; // openapi-typescript output
type User = components["schemas"]["User"];
export const handlers = [
http.get("/api/users/:id", ({ params }) => {
return HttpResponse.json<User>({ id: String(params.id), name: "Ada Lovelace", role: "admin" });
}),
http.post("/api/orders", async ({ request }) => {
const body = (await request.json()) as { items: unknown[] };
if (!body.items?.length) {
return HttpResponse.json({ type: "about:blank", title: "No items" }, { status: 422 });
}
return HttpResponse.json({ id: "ord_1", status: "pending" }, { status: 201 });
}),
graphql.query("GetCart", () => HttpResponse.json({ data: { cart: { items: [] } } })),
];
// src/mocks/node.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
Test lifecycle
// vitest.setup.ts (or jest.setup.ts)
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "./src/mocks/node";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
onUnhandledRequest: "error" is non-negotiable: without it a missing handler silently hits
the network or hangs.
Per-test overrides
import { http, HttpResponse, delay } from "msw";
import { server } from "../mocks/node";
it("shows a retry button when the profile request fails", async () => {
server.use(http.get("/api/users/:id", () => HttpResponse.json({ title: "boom" }, { status: 500 })));
render(<Profile id="42" />);
expect(await screen.findByRole("button", { name: /retry/i })).toBeVisible();
});
it("shows a spinner while loading", async () => {
server.use(http.get("/api/users/:id", async () => { await delay(200); return HttpResponse.json({ id: "42", name: "A", role: "member" }); }));
render(<Profile id="42" />);
expect(screen.getByRole("progressbar")).toBeVisible();
});
| Scenario | Handler body |
|---|---|
| HTTP error | HttpResponse.json(problem, { status: 503 }) |
| Network failure (fetch rejects) | HttpResponse.error() |
| Slow response | await delay(ms); delay("infinite") for a hanging request |
| First call fails, retry succeeds | first server.use(http.get(url, failing, { once: true })) then shared handler applies |
| Let a request reach the real server | passthrough() (rarely right in tests) |
Asserting on requests
Prefer asserting on the UI or return value. When the request itself is the behaviour (an analytics call, the exact payload sent), capture it inside the handler:
let sent: unknown;
server.use(http.post("/api/orders", async ({ request }) => {
sent = await request.json();
return HttpResponse.json({ id: "ord_1", status: "pending" }, { status: 201 });
}));
await submitOrder();
expect(sent).toEqual({ items: [{ sku: "A1", qty: 2 }] });
Browser, dev and Storybook
npx msw init public/ --save # writes mockServiceWorker.js and records the dir in package.json
Start the worker before rendering, only in development:
if (import.meta.env.DEV && import.meta.env.VITE_MOCK_API === "true") {
const { worker } = await import("./mocks/browser");
await worker.start({ onUnhandledRequest: "bypass" });
}
For Storybook, msw-storybook-addon lets each story set parameters.msw.handlers. Commit
mockServiceWorker.js and regenerate it when MSW is upgraded.
Migrating MSW 1 to 2
| MSW 1 | MSW 2 |
|---|---|
rest.get(url, (req, res, ctx) => res(ctx.json(x))) | http.get(url, () => HttpResponse.json(x)) |
ctx.status(500) | { status: 500 } as the init argument |
req.params, req.body | ({ params, request }), await request.json() |
ctx.delay() | await delay() |
res.once(...) | { once: true } handler option |
res.networkError() | HttpResponse.error() |
Community codemods exist, but review every changed handler; resolver arguments changed
shape and response bodies are now standard Fetch API Response objects.
Verification
- Delete a handler and confirm the test fails with MSW’s unhandled request error.
- Change a field name in the real API types; typed handlers must fail to compile.
- Run tests with the network disabled (Codex sandboxes often are); nothing should time out.
Gotchas
- Jest with jsdom lacks the Fetch API globals MSW 2 needs (
TextEncoder,Response,ReadableStream). UsetestEnvironment: "jest-fixed-jsdom"or switch to Vitest. - Relative URLs in Node have no origin. Use the absolute base URL the client uses, or a
path pattern that matches any origin (
*/api/users/:id). server.useoverrides leak into later tests ifresetHandlers()is missing.- Mocks drift from the real API silently. Type them from the schema and add contract tests for the endpoints that matter.
- Mocking
fetchwithvi.fn()and also running MSW means MSW never sees the request.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "API Mocking with MSW"
short_description: "Mock the network, not your fetch client"
default_prompt: "$api-mocking-msw Replace our jest.mock('axios') calls with MSW handlers" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.