Navigation

trpc-msw

packages/trpc-msw (npm trpc-msw) answers a tRPC client's calls at the network, inside a test. A test registers a resolver per procedure through a proxy typed off the router — trpcMsw.resource.readResource.query(({ input }) => …) — and every request the client sends is served by a router built from those resolvers, on the consumer's own initTRPC result, through tRPC's own handlers. Nothing about tRPC's wire format is written in the package: batching, the transformer, the error formatter, FormData and octet input, method override, server-sent events and the WebSocket protocol are all tRPC's.

It replaced msw-trpc, an adapter between the same two engines that re-implemented the wire format by hand, and is the worked example of the absorption flow in dependency admission.

How a call is answered

flowchart TD
  client["tRPC client<br/>any link"]
  msw["msw interceptor"]
  http{"Transport"}
  bypass{"Any procedure<br/>registered?"}
  next["next msw handler<br/>or the network"]
  fetchHandler["fetchRequestHandler<br/>batch, SSE, FormData"]
  wssHandler["applyWSSHandler<br/>per-connection router"]
  router["mock router<br/>on the consumer's t"]
  registry["resolver registry<br/>read at call time"]
  client --> msw
  msw --> http
  http -->|HTTP| bypass
  bypass -->|"no, and onUnhandledProcedure is Bypass"| next
  bypass -->|yes, or Error| fetchHandler
  http -->|WebSocket| wssHandler
  fetchHandler --> router
  wssHandler --> router
  router --> registry
  • One HTTP handler matches everything under the endpoint and hands the request to fetchRequestHandler, over a router built from every path registered so far. A batch carries several procedures behind one url, which is why the unit is the endpoint and never the procedure.
  • Resolvers are read when a procedure is called, not when the router is built. A resolver a test replaces answers the next call on any transport, and a path whose resolver was cleared by reset answers tRPC's NOT_FOUND.
  • WebSockets are bridged by presenting each msw client connection as the ws.WebSocket tRPC's applyWSSHandler drives. tRPC binds a connection to the router it was accepted with, so registering a path the current router lacks closes the open connections on the spot; the client queues its next call, reconnects and resubscribes onto the new router. tRPC's reconnect notification is not used for this: the client reads it only on its next turn, and a call sent before then would reach the old router.
  • The mock router is built on the consumer's own t, so a thrown TRPCError reaches the client through the real error formatter, and a resolver returns the procedure's output before the transformer rather than its serialized envelope.

Upstream

Every msw-trpc issue and pull request, with its verdict. The four verdicts and what each owes are the triage on the dependency admission page; a defect's proof is the named test in packages/trpc-msw/src/createTRPCMsw.test.ts or the type test in src/models/TRPCMswRouterRecord.test-d.ts.

UpstreamVerdictProof
#50False positiveThe mock answers the transport, so it works under any client, TanStack's new API included
#48In scope — defectThe transformer comes from t, so a resolver returns plain data; "answers a batch through the transformer in both directions"
#43In scope — feature"#43 reads FormData mutation input"
#38In scope — defectInput decoding is tRPC's; "answers a batch through the transformer in both directions"
#37In scope — featureTyped on tRPC 11's public inference helpers only, never unstable-core-do-not-import
#35, #33, #13In scope — featureA resolver throws a TRPCError; "#13 rejects with the error the resolver throws, shaped by the real formatter"
#29False positiveReporter's tRPC prerelease; nested routers are "answers a batch through the transformer in both directions"
#28Out of scopeA test replacing global.fetch after msw patched it — the test setup's, answered in the thread
#27False positiveShipped in msw-trpc 2; the peer range here is msw 3's
#25In scope — defectBatching is served; the query's input type is "#25 query" in the type test
#24, #12, #9False positiveIntegration questions for Next, Ladle and React Query; the README's setup is framework-free
#23False positivenode's fetch rejects a relative url — the client's configuration, as the thread found
#22, #2In scope — defect"#22 mutation with an output parser" and "#2 context" in the type test
#20Out of scopeThe service worker starting after the first query — msw's deferred mounting
#19, PR #47, PR #44In scope — feature"#19 streams a subscription over a WebSocket, including one registered after the socket opened", and "streams a subscription as server-sent events"
#17False positiveFixed upstream in 1.3.3; resolvers here receive a decoded input, never a promise of one
#15In scope — defectESM-only, and the build's attw and publint passes fail a broken module type
#14Out of scopeA console.log inside a handler — msw's, as the reporter found
#8In scope — defect"answers a batch through the transformer in both directions"
#6In scope — defectResolvers are fully typed, so nothing reaches no-unsafe-argument; the type test
#4In scope — defectBoth engines are peers on their current majors
PR #51In scope — featureUnhandledProcedureAction.Bypass; "passes a request naming no registered procedure on when told to bypass"
PR #49In scope — featurecreateContext sees the request; "hands the resolver the context built from the request"
PR #46In scope — featureallowMethodOverride; "#46 answers a query sent as a POST"
PR #45, PR #36In scope — defect"#45 #36 answers a mutation with no input and no output"
PR #1In scope — defectThe proxy reads its last segment as the method, so a procedure named query registers; the batch test

Never ported: observable subscriptions. tRPC 11 deprecates them in favour of async generators, so a subscription resolver here returns an async iterable and nothing else.

Key files

FileRole
packages/trpc-msw/src/createTRPCMsw.tsThe factory — the HTTP and WebSocket handlers, the registry and the proxy
packages/trpc-msw/src/services/createMockRouter.tsThe router over the registered paths, reading each resolver when it is called
packages/trpc-msw/src/models/TRPCMswRouterRecord.tsThe registration proxy's type, walked from the router's record
packages/trpc-msw/src/models/MswWebSocketAdapter.tsAn msw client connection presented as the socket tRPC's handler drives
apps/web/app/services/trpc/mswTrpc.test.tsThe app's wiring — its endpoint, its rootConfig, and the vitest lifecycle
apps/web/server/trpc/rootConfig.tsThe transformer and error formatter the server and the mock router share

Sources

  • maloguertin/msw-trpc — the package replaced, its issues and pull requests.
  • Fetch adapter (tRPC) — fetchRequestHandler, which serves batches, method override and server-sent events.
  • WebSockets (tRPC) — applyWSSHandler, the connection handler and the reconnect notification.
  • Subscriptions (tRPC) — async generators as the subscription shape, observables deprecated.
  • Mocking WebSocket (msw) — ws.link and the client connection the adapter wraps.

Details

Command palette

Keyboard shortcuts