Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@tanstack/svelte-query": "5.90.2",
"@tanstack/vue-query": "5.92.9",
"arktype": "2.2.0",
"msw": "2.10.2",
"nuxt": "3.21.0",
"swr": "2.4.1",
"tsx": "4.21.0",
Expand Down
1 change: 1 addition & 0 deletions dev/typescript/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const presets = {
},
},
],
msw: () => ['@hey-api/typescript', 'msw'],
sdk: () => [
/** SDK with types */
'@hey-api/typescript',
Expand Down
219 changes: 215 additions & 4 deletions docs/openapi-ts/plugins/msw.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,226 @@ description: MSW plugin for Hey API. Compatible with all our features.
---

<script setup lang="ts">
import FeatureStatus from '@components/FeatureStatus.vue';
</script>

# MSW <span data-soon>soon</span>

<FeatureStatus issueNumber=1486 name="MSW" />
# MSW

### About

[MSW](https://mswjs.io) is an API mocking library that allows you to write client-agnostic mocks and reuse them across any frameworks, tools, and environments.

The MSW plugin for Hey API generates type-safe mock handler factories from your OpenAPI spec, removing the tedious work of defining mock endpoints and ensuring your mocks stay in sync with your API.

## Features

- type-safe mock handlers generated from your OpenAPI spec
- seamless integration with `@hey-api/openapi-ts` ecosystem
- support for static response values or custom MSW resolver functions
- `ofAll` helper to generate handlers for every operation at once
- typed path parameters and request bodies
- automatic base URL inference from OpenAPI `servers` field
- minimal learning curve thanks to extending the underlying technology

## Installation

::: warning
MSW plugin requires `msw@^2` as a peer dependency. Make sure to install it in your project.
:::

In your [configuration](/openapi-ts/get-started), add `msw` to your plugins and you'll be ready to generate MSW artifacts. :tada:

```js
export default {
// ...other options
plugins: [
// ...other plugins
'msw', // [!code ++]
],
};
```

## Output

The MSW plugin will generate a `msw.gen.ts` file containing the following artifacts.

### Handler Factory

The `createMswHandlerFactory` function is the main export. It returns a `MswHandlerFactory` object with an `of` property containing a handler creator for each operation in your spec, and an `ofAll` method to generate handlers for all operations at once.

```ts
import { createMswHandlerFactory } from './client/msw.gen';

const createMock = createMswHandlerFactory({
baseUrl: 'http://localhost:8000', // optional, inferred from spec servers
});
```

If your OpenAPI spec defines a `servers` field, the base URL will be inferred automatically. You can override it by passing `baseUrl` in the configuration.

### Handler Creators

Each operation becomes a handler creator on the `of` object. Handler creators accept either a static response object with `status` and `result` properties, or a custom MSW `HttpResponseResolver` function. All handler creators also accept an optional `options` parameter of type `RequestHandlerOptions` from MSW.

## Usage

### Static Response

The simplest way to mock an endpoint is to provide a static response object with `status` and `result` properties. Both properties are type-checked against the operation's response types.

```ts
import { setupServer } from 'msw/node';
import { createMswHandlerFactory } from './client/msw.gen';

const createMock = createMswHandlerFactory();

const mockServer = setupServer(
// provide a static response with status code and result
createMock.of.getPetById({ status: 200, result: { id: 1, name: 'Fido' } }),

// type error if status or result type is incorrect
// @ts-expect-error
createMock.of.getPetById({ status: 200, result: 'wrong type' }),
);
```

### Custom Resolver

For more control, pass an MSW `HttpResponseResolver` function. The resolver receives typed path parameters and request body when available.

```ts
import { delay, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { createMswHandlerFactory } from './client/msw.gen';

const createMock = createMswHandlerFactory();

const mockServer = setupServer(
// custom resolver with typed params and body
createMock.of.updatePet(async ({ request, params }) => {
const body = await request.json();
return HttpResponse.json({ id: Number(params.petId), ...body }, { status: 200 });
}),

// async resolver with delay
createMock.of.getPetById(async () => {
await delay(100);
return HttpResponse.json({ id: 1, name: 'Fido' });
}),
);
```

::: tip
Path parameters are typed as `string | ReadonlyArray<string>` because MSW normalizes all path parameters to strings. Use `Number()` or similar conversions if you need numeric values.
:::

### Operations Without Responses

For operations that don't define a response type, the handler creator can be invoked without arguments or with a custom resolver function.

```ts
const mockServer = setupServer(
createMock.of.deletePet(),

createMock.of.deletePet(() => new HttpResponse(null, { status: 204 })),
);
```

### Response Examples

When your OpenAPI spec includes response examples, the generated handlers will use them as default values. This means you can call the handler without arguments and it will return the example response automatically.

```ts
const mockServer = setupServer(
// uses the example response from the OpenAPI spec as default
createMock.of.getFoo(),

// you can still override with a custom response
createMock.of.getFoo({ status: 200, result: { name: 'Custom' } }),
);
```

By default, `valueSources` is set to `['example']`, which embeds OpenAPI examples in the generated output. To disable this, set `valueSources` to an empty array.

::: code-group

```js [config]
export default {
// ...other options
plugins: [
// ...other plugins
{
name: 'msw',
valueSources: [], // [!code ++]
},
],
};
```

:::

### All Handlers (`ofAll`)

The `ofAll` method generates handlers for all operations at once. This is useful for quickly setting up a mock server without manually listing each operation.

```ts
import { setupServer } from 'msw/node';
import { createMswHandlerFactory } from './client/msw.gen';

const createMock = createMswHandlerFactory();

const server = setupServer(...createMock.ofAll());
```

#### `onMissingMock`

Some operations require a response argument (because they have no default example value). The `onMissingMock` option controls what happens for these operations:

- `'skip'` (default) — skips handlers that require an argument, only including operations that have default values or no response type
- `'error'` — includes all handlers, but operations without a provided response return a `501` error with the message `'[heyapi-msw] The mock of this request is not implemented.'`

```ts
// strict mode: all endpoints are mocked, missing ones return 501
const server = setupServer(...createMock.ofAll({ onMissingMock: 'error' }));
```

#### `overrides`

Use `overrides` to provide static responses for specific operations. The keys are operation IDs and the values are the same as what you'd pass to the individual `of` handler creator.

```ts
const server = setupServer(
...createMock.ofAll({
onMissingMock: 'skip',
overrides: {
getPetById: {
status: 200,
result: { id: 1, name: 'Fido', photoUrls: [] },
},
},
}),
);
```

When an override is provided for a required handler, it will always be included regardless of the `onMissingMock` setting.

### Handler Options

[Handler options](https://mswjs.io/docs/api/http#handler-options) can be provided. The object will be passed on to MSW helpers.

```ts
const mockServer = setupServer(
createMock.of.getPetById({ status: 200, result: { id: 1, name: 'Fido' } }, { once: true }),
);
```

## Known Limitations

- Query parameters are not typed in the resolver. MSW doesn't provide typed query params natively — use `new URL(request.url).searchParams` instead.
- The response type generic is omitted from `HttpResponseResolver` to avoid MSW's `DefaultBodyType` constraint issues with union and void response types.

## API

You can view the complete list of options in the [UserConfig](https://github.com/hey-api/openapi-ts/blob/main/packages/openapi-ts/src/plugins/msw/types.ts) interface.

<!--@include: ../../partials/examples.md-->
<!--@include: ../../partials/sponsors.md-->
1 change: 1 addition & 0 deletions examples/openapi-ts-fetch/openapi-ts.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export default defineConfig({
enums: 'javascript',
name: '@hey-api/typescript',
},
'msw',
],
});
5 changes: 4 additions & 1 deletion examples/openapi-ts-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"openapi-ts": "openapi-ts",
"preview": "vite preview",
"test": "vitest run",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
Expand All @@ -29,10 +30,12 @@
"eslint": "9.17.0",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.7",
"msw": "2.10.2",
"oxfmt": "0.27.0",
"postcss": "8.4.41",
"tailwindcss": "3.4.9",
"typescript": "5.9.3",
"vite": "7.3.1"
"vite": "7.3.1",
"vitest": "4.0.18"
}
}
Loading
Loading