Skip to content

Commit c054d7d

Browse files
authored
feat(msw): add @orpc/experimental-msw for typed MSW request handlers (#1899)
Adds `@orpc/experimental-msw`, an official [MSW](https://mswjs.io/) integration. `createHTTPUtils(contract, { prefix, handler })` turns a router contract (or an implemented router) into fully typed, per-procedure MSW request handlers: `mock.planet.list.handler(...)`, `mock.planet.find.error('NOT_FOUND', ...)`, `mock.planet.list.loading()` for Storybook loading states, and `mock.planet.find.passthrough()` to let specific procedures hit the real server. The package reimplements neither the wire format nor routing: each mock is a single-procedure router built with `implement()` and served through the fetch handler the user creates via the required `handler` option, typically their production `RPCHandler`/`OpenAPIHandler` configuration. MSW matches broadly on `origin` + `prefix` (any origin by default, so mocks work in Node without configuration), and the created handler decides everything else: requests it matches are answered with real serialization, validation, and error envelopes; requests it does not match fall through to other MSW handlers, preserving MSW's composition model for Storybook story-level overrides. Protocol changes in core, including routing features, are picked up automatically; the package only owns DX. ## Features - Handlers reuse `@orpc/server`'s own `ProcedureHandler` type, so mocks are typed end to end from the contract: parsed input, output checked against the schema, and `.error` restricted to the codes declared via `.errors({...})` (arbitrary `ORPCError`s can be thrown from `.handler`). - Works with any protocol the created handler speaks; method mismatches and unknown paths fall through to other MSW handlers. - The `context` option (value or per-request factory receiving the MSW resolver info) types the mock handlers' `context`, feeds context-driven plugins such as `ResponseHeadersHandlerPlugin`, and becomes required when its type cannot be satisfied by an empty object. - Matching never reads the request body; mocks consume it like any MSW resolver, `.passthrough()` consumes only a clone so the original replays intact, and pending `.loading()` mocks reject on abort to release resources. - Named `createHTTPUtils` (mirroring MSW's `http` vs `ws` namespaces) and published as `@orpc/experimental-msw` while the API settles. ## Testing - 25 runtime tests drive real `RPCLink` and `OpenAPILink` clients through `msw/node` with 100% coverage on all metrics, covering serialization fidelity, typed errors over both protocols, validation failures, event iterator streaming, OpenAPI path/greedy/prefixed routes, default and wildcard origins, fall-through, passthrough body replay, abort-rejected loading, plugin-configured handlers, and context-driven response headers. - Type tests cover input/output typing, error code restrictions, implemented-router support, and context inference/requirement rules. - `pnpm type:check`, lint, sherif, `docs:validate`, and `unbuild` all pass. ## Docs - New `docs/integrations/msw` page, plus a "Mocking Network Requests" section in the Testing & Mocking guide. - README package lists updated across the monorepo.
1 parent 914cf25 commit c054d7d

43 files changed

Lines changed: 1668 additions & 11 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ You can read the documentation [here](https://orpc.dev).
5454
- [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
5555
- [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
5656
- [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57+
- [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
5758
- [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
5859
- [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
5960
- [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.

apps/content/docs/advanced/testing-and-mocking.mdx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: "Testing and Mocking"
3-
description: "Learn how to test oRPC procedures directly with server-side clients and create mock implementations with the implementer."
3+
description: "Learn how to test oRPC procedures directly with server-side clients, create mock implementations with the implementer, and mock network requests with MSW."
44
---
55

66
## Testing
@@ -45,3 +45,21 @@ Use `fakeListPlanet` anywhere your test would normally use the real `listPlanet`
4545
:::warning
4646
`implement` does not support [lazy routers](/docs/router#lazy-router) directly. If you need to mock one, first [unlazy the router](/docs/contract/router#router-to-contract).
4747
:::
48+
49+
## Mocking Network Requests
50+
51+
To mock at the network level instead, for example in [Storybook](https://storybook.js.org/) stories or browser tests, use the [MSW Integration](/docs/integrations/msw). It turns a contract or router into typed [MSW](https://mswjs.io/) request handlers served through a real fetch handler:
52+
53+
```ts
54+
import { createHTTPUtils } from '@orpc/experimental-msw'
55+
import { RPCHandler } from '@orpc/server/fetch'
56+
57+
const mock = createHTTPUtils(router, {
58+
prefix: '/rpc',
59+
handler: router => new RPCHandler(router),
60+
})
61+
62+
const handlers = [
63+
mock.planet.list.handler(() => []),
64+
]
65+
```
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
title: "MSW Integration"
3+
description: "Mock oRPC procedures at the network level with typed MSW request handlers, reusing the real RPC or OpenAPI runtime for serialization and validation."
4+
sidebar:
5+
label: "MSW"
6+
---
7+
8+
:::warning
9+
This guide assumes you are already familiar with [MSW](https://mswjs.io/). If you need a refresher, review the official MSW documentation before continuing.
10+
:::
11+
12+
## Installation
13+
14+
```package-install
15+
npm install @orpc/experimental-msw@beta
16+
```
17+
18+
## Setup
19+
20+
Create MSW utils from a [router contract](/docs/contract/router) or an implemented [router](/docs/router) (convert [lazy routers](/docs/router#lazy-router) with [`unlazyRouter`](/docs/contract/router#router-to-contract) first). The `handler` option creates the fetch handler that serves each mock. Configure it like your production handler, so serialization, validation, and error envelopes behave exactly like your real server.
21+
22+
```ts
23+
import { createHTTPUtils } from '@orpc/experimental-msw'
24+
import { RPCHandler } from '@orpc/server/fetch'
25+
26+
export const mock = createHTTPUtils(contract, {
27+
prefix: '/rpc',
28+
handler: router => new RPCHandler(router),
29+
})
30+
```
31+
32+
Set `prefix` to the prefix your link sends requests to. Any origin matches by default; narrow it with the `origin` option, which supports MSW wildcards.
33+
34+
:::tip
35+
Any protocol works: pair the [RPCLink](/docs/rpc/link) with a [RPCHandler](/docs/rpc/handler), or the [OpenAPILink](/docs/openapi/link) with an [OpenAPIHandler](/docs/openapi/handler).
36+
:::
37+
38+
## Mocking Procedures
39+
40+
The `.handler` method creates an MSW request handler that resolves a procedure. The input your mock receives and the output it returns are validated and serialized by the created fetch handler, exactly like on a real server.
41+
42+
```ts
43+
import { setupServer } from 'msw/node'
44+
45+
const server = setupServer(
46+
mock.planet.list.handler(({ input }) => [
47+
{ id: 1, name: 'Earth' },
48+
]),
49+
)
50+
51+
server.listen()
52+
```
53+
54+
To access request details, such as the raw `request`, expose them through the [`context` option](#advanced-configuration).
55+
56+
:::info
57+
[AsyncIteratorObject](/docs/async-iterator-object) outputs work too: return an async generator and the client receives a streamed response.
58+
:::
59+
60+
## Mocking Errors
61+
62+
The `.error` method creates an MSW request handler that rejects a procedure with one of its [defined errors](/docs/contract/procedure#typesafe-errors), serialized exactly like a server-thrown error. For dynamic or arbitrary errors, use `.handler` and throw the `errors` constructors or any [`ORPCError`](/docs/error-handling#orpcerror-class):
63+
64+
```ts
65+
import { ORPCError } from '@orpc/client'
66+
67+
const handlers = [
68+
mock.planet.find.error('NOT_FOUND', { data: { id: 123 } }),
69+
mock.planet.update.handler(({ input, errors }) => {
70+
throw errors.CONFLICT({ data: { id: input.id } })
71+
}),
72+
mock.planet.delete.handler(() => {
73+
throw new ORPCError('SERVICE_UNAVAILABLE')
74+
}),
75+
]
76+
```
77+
78+
## Mocking Loading States
79+
80+
The `.loading` method creates an MSW request handler that never resolves, useful for testing loading states, for example in [Storybook](https://storybook.js.org/docs/writing-stories/mocking-data-and-modules/mocking-network-requests) stories:
81+
82+
```ts
83+
export const Loading: Story = {
84+
parameters: {
85+
msw: {
86+
handlers: [mock.planet.list.loading()],
87+
},
88+
},
89+
}
90+
```
91+
92+
## Passthrough
93+
94+
The `.passthrough` method creates an MSW request handler that performs matching requests against the real server as-is, useful to exempt specific procedures from mocking, for example while [onUnhandledRequest](https://mswjs.io/docs/api/setup-server/listen#onunhandledrequest) treats everything else as an error:
95+
96+
```ts
97+
const handlers = [
98+
mock.planet.list.handler(() => []),
99+
mock.planet.find.passthrough(), // hits the real server
100+
]
101+
```
102+
103+
## Advanced Configuration
104+
105+
All handler behavior is configured through the `handler` option, so mocks can mirror your production setup exactly, such as plugins, a custom serializer, or `allowMethods` if your client sends [GET requests](/docs/rpc/handler#supported-http-methods) over the RPC protocol:
106+
107+
```ts
108+
import { RPCHandler } from '@orpc/server/fetch'
109+
import { ResponseHeadersHandlerPlugin } from '@orpc/server/plugins'
110+
111+
const mock = createHTTPUtils(contract, {
112+
prefix: '/rpc',
113+
handler: router => new RPCHandler(router, {
114+
plugins: [new ResponseHeadersHandlerPlugin()],
115+
}),
116+
})
117+
```
118+
119+
The `context` option controls the [context](/docs/context) passed to the created handler on each request, and mock handlers receive it as `context`, enabling context-driven behaviors such as the [Response Headers Plugin](/docs/plugins/response-headers).
120+
121+
```ts
122+
import { ResponseHeadersHandlerPlugin, type ResponseHeadersHandlerPluginContext } from '@orpc/server/plugins'
123+
124+
interface MockServerContext extends ResponseHeadersHandlerPluginContext {
125+
reqHeaders: Headers
126+
}
127+
128+
const mock = createHTTPUtils(contract, {
129+
context: (info): MockServerContext => ({ reqHeaders: info.request.headers }),
130+
handler: router => new RPCHandler(router, {
131+
plugins: [new ResponseHeadersHandlerPlugin()],
132+
}),
133+
})
134+
135+
const handlers = [
136+
mock.planet.list.handler(({ context }) => {
137+
const locale = context.reqHeaders.get('accept-language') ?? 'en'
138+
context.resHeaders?.set('content-language', locale)
139+
return []
140+
}),
141+
]
142+
```
143+
144+
You can also disable input or output validation of the mocked data:
145+
146+
```ts
147+
const mock = createHTTPUtils(contract, {
148+
handler: router => new RPCHandler(router),
149+
disableInputValidation: true,
150+
disableOutputValidation: true,
151+
})
152+
```
153+
154+
Each mock serves a router containing only the procedure being mocked. Requests the created handler does not match simply fall through to other MSW handlers.
155+
156+
## Limitations
157+
158+
Requests sent through the [Batch Requests Plugin](/docs/plugins/batch) cannot be mocked. Each mock serves a router containing only its own procedure, so even a `handler` configured with the batch plugin cannot resolve the other procedures bundled into the same HTTP request. Disable batching when mocking with MSW.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@orpc/contract": "workspace:*",
2727
"@orpc/evlog": "workspace:*",
2828
"@orpc/experimental-effect": "workspace:*",
29+
"@orpc/experimental-msw": "workspace:*",
2930
"@orpc/hibernation": "workspace:*",
3031
"@orpc/json-schema": "workspace:*",
3132
"@orpc/next": "workspace:*",

packages/ai-sdk/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ You can read the documentation [here](https://orpc.dev).
5454
- [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
5555
- [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
5656
- [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57+
- [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
5758
- [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
5859
- [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
5960
- [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.

packages/arktype/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ You can read the documentation [here](https://orpc.dev).
5454
- [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
5555
- [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
5656
- [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57+
- [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
5758
- [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
5859
- [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
5960
- [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.

packages/bun/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ You can read the documentation [here](https://orpc.dev).
5151
- [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
5252
- [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
5353
- [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
54+
- [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
5455
- [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
5556
- [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
5657
- [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.

packages/client/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ You can read the documentation [here](https://orpc.dev).
5454
- [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
5555
- [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
5656
- [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57+
- [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
5758
- [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
5859
- [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
5960
- [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.

packages/cloudflare/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ You can read the documentation [here](https://orpc.dev).
5454
- [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
5555
- [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
5656
- [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57+
- [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
5758
- [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
5859
- [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
5960
- [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.

packages/contract/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ You can read the documentation [here](https://orpc.dev).
5454
- [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
5555
- [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
5656
- [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57+
- [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
5758
- [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
5859
- [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
5960
- [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.

0 commit comments

Comments
 (0)