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.

  • Testing & QA
  • $api-mocking-msw
  • 180 lines
  • 2 files
  • ~1,750 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- api-mocking-msw

Installs to ~/.agents/skills/api-mocking-msw/ for every project on this machine.

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

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.

~/.agents/skills/api-mocking-msw/SKILL.md

--- name: api-mocking-msw description: Mocks HTTP and GraphQL at the network boundary with Mock Service Worker (MSW 2.x http and graphql handlers, HttpResponse, setupServer in Node,… license: MIT ---

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

  1. Read AGENTS.md and package.json: MSW version (msw@^2 assumed below), test runner (Vitest, Jest), environment (jsdom, happy-dom, node), Storybook presence.
  2. Find existing mocks: rg -n "msw|setupServer|jest.mock\(.*(axios|fetch)|vi.mock\(|nock\(" -g '!node_modules'.
  3. Find the API contract: OpenAPI spec, generated client types, GraphQL schema. Mocks typed against it catch drift; hand-written shapes rot.
  4. Installing MSW needs network; MSW 2 needs Node 18+.

When MSW is the right tool

SituationUse
Frontend or Node code using fetch, axios, ky, Apollo, urqlMSW
Python with requestsresponses
Python with httpxrespx
Node legacy http module code, or MSW cannot be addednock
JVM services, or language-agnostic stub server in DockerWireMock
Verifying the mock matches the providerContract tests (contract-testing), not more mocks
You own the backend and can run itIntegration 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();
});
ScenarioHandler body
HTTP errorHttpResponse.json(problem, { status: 503 })
Network failure (fetch rejects)HttpResponse.error()
Slow responseawait delay(ms); delay("infinite") for a hanging request
First call fails, retry succeedsfirst server.use(http.get(url, failing, { once: true })) then shared handler applies
Let a request reach the real serverpassthrough() (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 1MSW 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). Use testEnvironment: "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.use overrides leak into later tests if resetHandlers() is missing.
  • Mocks drift from the real API silently. Type them from the schema and add contract tests for the endpoints that matter.
  • Mocking fetch with vi.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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem