Skip to content

feat: implement msw plugin#3570

Open
malcolm-kee wants to merge 7842 commits into
hey-api:mainfrom
malcolm-kee:feat/msw-plugin
Open

feat: implement msw plugin#3570
malcolm-kee wants to merge 7842 commits into
hey-api:mainfrom
malcolm-kee:feat/msw-plugin

Conversation

@malcolm-kee

@malcolm-kee malcolm-kee commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Closes #1486

Summary

Implement msw plugin that generates a msw.gen.ts file with type-safe mock handler factories from OpenAPI specs. Each operation is exported as a named handler creator (<operationId>Mock) with a wildcard base URL, plus a getAllMocks helper to generate handlers for all operations at once. A createMswHandlerFactory function is also exported for custom base URL binding.

Important

Even though many expect fake data generation is part of this plugin, that probably overlaps with faker plugin. The only mock data handled by this plugin at the moment is the example defined in the OpenAPI spec.

API Design

Configuration

export default {
  plugins: [
    {
      name: "msw",
      valueSources: ["example"], // set to [] to disable example embedding
    },
  ],
};

Usage

Individual handler exports (wildcard base URL)

import { HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { getPetByIdMock, updatePetMock, getInventoryMock, getAllMocks } from "./client/msw.gen";

const server = setupServer(
  // Static response — type-checked result, status defaults to dominant success code
  getPetByIdMock({ result: { id: 1, name: "Fido", photoUrls: [] } }),

  // Explicit status code
  getPetByIdMock({ status: 200, result: { id: 1, name: "Fido", photoUrls: [] } }),

  // Custom resolver — params and request body are typed
  updatePetMock(async ({ request, params }) => {
    const body = await request.json();
    return HttpResponse.json({ id: Number(params.petId), ...body }, { status: 200 });
  }),

  // Operations with spec examples — no args needed
  getInventoryMock(),
);

Handler options

MSW handler options can be passed as a second argument:

getPetByIdMock({ result: { id: 1, name: "Fido" } }, { once: true });

Custom base URL (createMswHandlerFactory)

import { createMswHandlerFactory } from "./client/msw.gen";

// Explicit base URL
const createMock = createMswHandlerFactory({
  baseUrl: "http://localhost:3000",
});

// No args — infers base URL from spec's servers field
const createMock2 = createMswHandlerFactory();

const server = setupServer(
  createMock.getPetByIdMock({ result: { id: 1, name: "Fido", photoUrls: [] } }),
);

All handlers (getAllMocks)

import { getAllMocks } from "./client/msw.gen";

// Quick setup — all operations with defaults
setupServer(...getAllMocks());

// Strict mode — missing mocks return 501
setupServer(...getAllMocks({ onMissingMock: "error" }));

// With overrides — keys are handler names (<operationId>Mock)
setupServer(
  ...getAllMocks({
    onMissingMock: "skip",
    overrides: {
      getPetByIdMock: {
        result: { id: 1, name: "Fido", photoUrls: [] },
      },
    },
  }),
);

Design decisions

Why <operationId>Mock naming? — Appending Mock avoids naming collisions with other generated artifacts (types, SDK functions) while keeping the handler clearly associated with its operation.

Why both individual exports and createMswHandlerFactory? — Individual exports use a wildcard (*) base URL for zero-config convenience. The factory function allows binding to a specific base URL when needed (e.g. integration tests against a specific server).

Why valueSources instead of example: boolean? — Extensible for future sources (e.g. ['example', 'faker'] when faker plugin is ready).

onMissingMock — Operations that require a response argument (no default example) are either skipped ('skip') or return a 501 ('error'). Overrides always take precedence.

Handler creator signatures

Operation has Parameter type Optional?
Response type with status codes { result, status? } | ToResponseUnion<Responses> | HttpResponseResolver<PathParams, Body> No*
Response type, void { result, status? } | ToResponseUnion<Responses> | HttpResponseResolver<PathParams, Body> Yes
No response type (no status code) HttpResponseResolver<PathParams, Body> Yes

* Optional if the spec defines an example for the dominant response.

Response method selection

Content type Method
application/json HttpResponse.json()
text/* HttpResponse.text()
binary/octet-stream new HttpResponse()
void / no content new HttpResponse(null)

When multiple 2xx responses exist, the dominant one is chosen by priority: json > text > binary > void.

Known limitations

  • Response type generic is omitted from HttpResponseResolver to avoid MSW's DefaultBodyType constraint issues with union/void response types
  • Query parameters are not typed in resolvers (MSW doesn't support typed query params natively)
  • Only 2xx responses are considered for the dominant response

@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pullfrog

pullfrog Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Error

agent completed without reporting progress

Pullfrog  | Rerun failed job ➔View workflow run | Triggered by Pullfrogpullfrog.com𝕏

@vercel

vercel Bot commented Mar 13, 2026

Copy link
Copy Markdown

@malcolm-kee is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Mar 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 25fdbb9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hey-api/openapi-ts Patch
@hey-api/shared Patch
@hey-api/openapi-python Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. feature 🚀 Feature request. labels Mar 13, 2026
@malcolm-kee malcolm-kee mentioned this pull request Mar 13, 2026
4 tasks
@codecov

codecov Bot commented Mar 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 18.44262% with 398 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.42%. Comparing base (2808f39) to head (25fdbb9).
⚠️ Report is 154 commits behind head on main.

Files with missing lines Patch % Lines
examples/openapi-ts-fetch/src/client/msw.gen.ts 29.83% 112 Missing and 55 partials ⚠️
...kages/openapi-ts/src/plugins/msw/shared/handler.ts 1.58% 54 Missing and 8 partials ⚠️
.../src/plugins/msw/shared/computeDominantResponse.ts 8.92% 33 Missing and 18 partials ⚠️
packages/openapi-ts/src/plugins/msw/v2/plugin.ts 2.00% 48 Missing and 1 partial ⚠️
packages/openapi-ts/src/plugins/msw/shared/sort.ts 0.00% 14 Missing and 3 partials ⚠️
...ages/openapi-ts/src/plugins/msw/shared/response.ts 0.00% 13 Missing and 1 partial ⚠️
packages/shared/src/utils/url.ts 0.00% 5 Missing and 9 partials ⚠️
...ackages/openapi-ts/src/plugins/msw/shared/types.ts 0.00% 10 Missing ⚠️
packages/openapi-ts/src/plugins/msw/config.ts 28.57% 3 Missing and 2 partials ⚠️
packages/openapi-ts/src/plugins/msw/shared/path.ts 0.00% 4 Missing and 1 partial ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3570      +/-   ##
==========================================
- Coverage   40.08%   39.42%   -0.67%     
==========================================
  Files         522      545      +23     
  Lines       19323    20459    +1136     
  Branches     5761     6175     +414     
==========================================
+ Hits         7746     8065     +319     
- Misses       9374     9989     +615     
- Partials     2203     2405     +202     
Flag Coverage Δ
unittests 39.42% <18.44%> (-0.67%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mrlubos

mrlubos commented Mar 13, 2026

Copy link
Copy Markdown
Member

@malcolm-kee Before I go into it, two questions:

  1. How much AI was used to create this pull request?
  2. How much are you willing to improve it? i.e. Is this the final version?

@malcolm-kee

Copy link
Copy Markdown
Contributor Author

@mrlubos I come up with the API design and AI was doing most of the implementations while I watch.

Not final. I'm happy to iterate on this, just want some progress on this plugin.

@malcolm-kee

Copy link
Copy Markdown
Contributor Author

The diff is big is mostly because of the tests and snapshots.

@pkg-pr-new

pkg-pr-new Bot commented Mar 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3570

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3570

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3570

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3570

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3570

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3570

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3570

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3570

commit: 25fdbb9

@malcolm-kee

Copy link
Copy Markdown
Contributor Author

I did some refactoring/enhancements:

  • remove duplications of type definition and function param definition. It's only defined at type level and the implementation param is auto-inferred from that.
  • extract example from schema as default value.

Comment thread packages/openapi-ts/src/plugins/msw/classifyResponse.ts Outdated
@malcolm-kee

malcolm-kee commented Mar 14, 2026

Copy link
Copy Markdown
Contributor Author

@mrlubos I manually validated the code and made some refactoring. It would be great if you can provide some feedbacks, especially on the API.

@malcolm-kee malcolm-kee force-pushed the feat/msw-plugin branch 2 times, most recently from ac3c300 to b34b5bd Compare March 15, 2026 04:36
@malcolm-kee

malcolm-kee commented Mar 15, 2026

Copy link
Copy Markdown
Contributor Author

More revision:

  • Redesign the API - the static parameter becomes { status: number; result: ResultType } instead of just ResultType. This design allows typescript to infer the types better with a stable object type with explicit properties instead of a more generic ResultType. This also make it easier to overwrite the status code without falling back to use the custom resolver approach.
  • Forward the handler options to msw, so no lost of capabilities.
  • Added resolveToNull helper function to remove duplicate fallbacks

@malcolm-kee

Copy link
Copy Markdown
Contributor Author

Added examples options to the plugin.

@malcolm-kee

Copy link
Copy Markdown
Contributor Author

Change plugin options from examples: boolean to valueSources: Array<'example'>, so that when fakerjs is ready, we just need to switch the default value to valueSources: ['example', 'faker'].

@malcolm-kee malcolm-kee force-pushed the feat/msw-plugin branch 2 times, most recently from e3e18f0 to 7bb2ae4 Compare March 16, 2026 09:20
@malcolm-kee

malcolm-kee commented Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

Ideas on how to continue enhancing this PR, in case anyone want to take over this, since I might not be free to iterate on this:

Implement ofAll

We can actually implement ofAll helper without waiting for faker plugin, by providing options to customize its behavior:

const createMock = createMswHandlerFactory();

const allMockHandlers = createMock.ofAll({
  onMissingMock: 'skip', // 'skip' | 'error',
  overrides: {
    getPetById({
      status: 200,
      result: { id: 1, name: "Fido", photoUrls: [] },
    })
  }
});

const server = setupServer(...allMockHandlers);

onMissingMock option:

  • skip: we will not include the MSW handlers that requires argument in the returned array of requestHandlers. This is possible because we can infer if argument is required for a handler (I differentiate them by providing them different types - HttpHandlerFactory are those require argument while OptionalHttpHandlerFactory are those does not require argument)
  • error: we will include the MSW handlers for all, but for those require argument, we will return HttpResponse('[heyapi-msw] The mock of this request is not implemented.', { status: 501 })

overrides option API is similar to the API of single handler, but instead of calling individual helper, user can define all of them at once here.

pullfrog Bot and others added 26 commits April 14, 2026 06:55
Add automatic bot filtering to update-contributors.sh:
- Skip logins ending with [bot] suffix
- Skip accounts with GitHub API type "Bot"

Remove duplicate pullfrog[bot] entries from contributors-list.md.

Closes hey-api#3764
…-options-headers-type

fix(client-fetch): narrow headers to Headers in ResolvedRequestOptions
…ve-preview-7.x

chore(deps): update dependency @typescript/native-preview to v7.0.0-dev.20260411.1
chore(deps): update dependency msw to v2.13.2
…plugins

feat: warn when duplicate plugins are specified
@malcolm-kee

Copy link
Copy Markdown
Contributor Author

Hi @mrlubos is there anything that I can do to help to make this ready for merge?

@mrlubos

mrlubos commented May 2, 2026

Copy link
Copy Markdown
Member

@malcolm-kee the next step would be to finish the examples plugin #3772

The way I imagine it working is each component (schema, parameter, etc) generates a getter function which would return its example(s). If there are none, no function is generated. Operations would also generate a getter function, with the support for discrimination by status code.

The MSW plugin would then need to call these functions and get the result. If the result is defined we'd return it, otherwise fall through.

That's the last big change I planned to do so we can remove the hard-coded values and support replacing them with Faker and more in the future.

I don't currently have time to polish it. If you want, open a pull request merging into the Examples plugin, I'd review it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 Feature request. size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MSW plugin

8 participants