Skip to content

MSW mocks

seampact writes Mock Service Worker 2 handlers for each operation of your contract. Your tests and your dev server run the fetch client against them, with no backend.

targets:
- id: types
use: "@seampact/codegen-ts"
apis: [notes]
- id: client
use: "@seampact/codegen-fetch"
apis: [notes]
link: { types: types }
- id: mocks
use: "@seampact/codegen-msw"
apis: [notes]
link: { types: types }

Install msw 2, then run npx seampact generate.

import { setupServer } from "msw/node";
import { createMocks } from "./__generated__/mocks/notes/index.js";
const mocks = createMocks({ baseUrl: "http://localhost" });
const server = setupServer(...mocks.handlers);

Give createMocks the same baseUrl as your client.

Two apis on one origin with overlapping paths: pass the handlers of the api with the more specific paths first to setupServer.

In the browser, use setupWorker from msw/browser, and copy the worker script once with npx msw init public.

A handler answers the route’s example from the contract. A route with no example has nothing to answer: the call gets a 500 that names the route, until your test gives a reply.

Each operation of mocks has three methods: reply(status, body), error(code, params) and resolve(function). Give them to server.use in a test (here with Vitest):

import { afterEach } from "vitest";
afterEach(() => server.resetHandlers());
server.use(mocks.getNote.reply(200, { noteId: "n1", title: "Hello", createdAt: "2026-01-01T00:00:00Z" }));
server.use(mocks.createNote.error("api.rate_limited", { retryAfterMs: 1500 }));
server.use(
mocks.getNote.resolve(({ params }) =>
params.noteId === "n1" ? { status: 200, body: { noteId: "n1", title: "Hi", createdAt: "2026-01-01T00:00:00Z" } } : { status: 404, code: "api.not_found" },
),
);

With Jest, the calls are the same. The status, body, error code and params are typed from the contract. A reply is written like your server writes it: the same envelope, the same page links, and errors in your error format.

  • The handlers do not check the request. The option validateRequests is refused until they do.
  • The default handler answers success examples only, never an error or a request example.
  • A form the fetch client cannot read yet is refused the same way here (for example wire.urls.absolute.unsupported).
  • Pages whose numbers or totals the server computes (the Laravel and Spring paging packs) are refused by name.

Codes: Generate errors.

Fetch client, the client your tests call.