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.
Add the target
Section titled “Add the target”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.
Start the handlers
Section titled “Start the handlers”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.
Answer in a test
Section titled “Answer in a test”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.
Limits
Section titled “Limits”- The handlers do not check the request. The option
validateRequestsis 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.