zod-mock-api
v1.0.1
Published
Type-safe mock servers for ts-rest, oRPC, and tRPC API contracts, powered by MSW and Zod
Downloads
184
Maintainers
Readme
zod-mock-api
Type-safe mock servers for ts-rest, oRPC, and tRPC API contracts, powered by MSW and Zod schema generation.
zod-mock-api automatically generates realistic fake responses from your Zod schemas so you can start testing immediately — then override individual endpoints with static values or callback functions when you need fine-grained control.
Features
- Zero boilerplate — pass your contract and get a running mock server.
- Auto-generated responses — every route is pre-populated with random data that satisfies its Zod schema (via
@anatine/zod-mock). - Full type safety — paths, status codes, request bodies, and response bodies are all inferred from the contract; typos are caught at compile time.
- Per-test overrides — override any route with a static response or a callback that receives the request info and a pre-generated default.
- Multi-framework — first-class support for ts-rest, oRPC, and tRPC contracts.
- Nested routers — deeply nested contracts are flattened automatically.
- All HTTP methods — GET, POST, PUT, PATCH, and DELETE (including bodyless responses).
- Escape hatch — access the underlying MSW
serverandhttphelper for custom handlers. - Adapter architecture — the generic MSW + Zod plumbing (
mockHttpApi) is separated from framework adapters, making it straightforward to add support for additional contract libraries.
Installation
Install the core package and its peer dependencies:
npm install --save-dev zod-mock-api @anatine/zod-mock @faker-js/faker msw zodThen install the peer dependency for the framework(s) you use:
# For ts-rest
npm install --save-dev @ts-rest/core
# For oRPC
npm install --save-dev @orpc/contract
# For tRPC
npm install --save-dev @trpc/serverNote:
@ts-rest/core,@orpc/contract, and@trpc/serverare all optional peer dependencies. You only need the one(s) matching the adapters you use.
Imports
You can import everything from the main entry point, or use the dedicated sub-path exports to only pull in the adapter you need:
// Everything (barrel export)
import { mockTsRest, mockOrpc, mockTrpc } from 'zod-mock-api';
// Per-adapter (tree-shakeable, avoids loading unused adapters)
import { mockTsRest } from 'zod-mock-api/ts-rest';
import { mockOrpc } from 'zod-mock-api/orpc';
import { mockTrpc } from 'zod-mock-api/trpc';
// Generic HTTP layer (for building custom adapters)
import { createMockServer, registerOverride } from 'zod-mock-api/http';Quick Start
ts-rest
import { initContract } from '@ts-rest/core';
import { mockTsRest } from 'zod-mock-api';
import { z } from 'zod';
const c = initContract();
const contract = c.router({
getUsers: {
method: 'GET',
path: '/users',
responses: {
200: z.array(z.object({ id: z.string(), name: z.string() })),
},
},
createUser: {
method: 'POST',
path: '/users',
body: z.object({ name: z.string() }),
responses: {
201: z.object({ id: z.string(), name: z.string() }),
},
},
});
// Create the mock server — all routes are immediately available
// with randomly generated responses.
const server = mockTsRest('http://localhost:3000', contract);
// Override specific endpoints as needed:
server.get('/users', 200, [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]);
// Use in your tests, then clean up:
server.close();oRPC
import { oc } from '@orpc/contract';
import { mockOrpc } from 'zod-mock-api';
import { z } from 'zod';
const contract = {
getUsers: oc
.route({ method: 'GET', path: '/users' })
.output(z.array(z.object({ id: z.string(), name: z.string() }))),
createUser: oc
.route({ method: 'POST', path: '/users', successStatus: 201 })
.input(z.object({ name: z.string() }))
.output(z.object({ id: z.string(), name: z.string() })),
};
// Create the mock server — all procedures are immediately available
// with randomly generated responses.
const server = mockOrpc('http://localhost:3000', contract);
// Override by procedure key (fully type-safe):
server.mock('getUsers', [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]);
// Use in your tests, then clean up:
server.close();tRPC
import { initTRPC } from '@trpc/server';
import { mockTrpc } from 'zod-mock-api';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.output(z.object({ id: z.string(), name: z.string() }))
.query(({ input }) => ({ id: input.id, name: 'Alice' })),
createUser: t.procedure
.input(z.object({ name: z.string() }))
.output(z.object({ id: z.string(), name: z.string() }))
.mutation(({ input }) => ({ id: '1', name: input.name })),
});
// Create the mock server — procedures with .output() Zod schemas
// get auto-generated responses. Include any tRPC path prefix in the host.
const server = mockTrpc('http://localhost:3000/trpc', appRouter);
// Override by procedure key (fully type-safe):
server.mock('getUser', { id: '1', name: 'Alice' });
// Use in your tests, then clean up:
server.close();Usage
ts-rest
Static Responses
The simplest override provides a literal response value:
server.get('/users', 200, [{ id: '1', name: 'Alice' }]);
server.post('/users', 201, { id: '2', name: 'Bob' });
server.put('/users/:id', 200, { id: '2', name: 'Bob Updated' });
server.patch('/users/:id', 200, { id: '2', name: 'Bob Patched' });Callback Resolvers
For dynamic responses, pass a callback. It receives the MSW request info
(with a typed json() method) and a pre-generated default response:
server.post('/users', 201, async ({ request }, defaultResponse) => {
const body = await request.json();
return {
...defaultResponse, // includes randomly generated fields
name: body.name, // override specific fields
};
});Error Status Codes
You can simulate error responses, including status codes not defined in your contract (e.g. 500 from a load balancer):
server.get('/users', 404, { error: 'Not found' });
server.get('/users', 500, { message: 'Internal server error' });DELETE with No Body
ts-rest's c.noBody() is fully supported:
server.delete('/users/:id', 204, undefined);oRPC
Static Responses
Override a procedure by its key path in the contract:
server.mock('getUsers', [{ id: '1', name: 'Alice' }]);
server.mock('nested.getUser', { id: '1', name: 'Alice' });Callback Resolvers
For dynamic responses, pass a callback with access to the request and a pre-generated default:
server.mock('createUser', async ({ request }, defaultResponse) => {
const body = await request.json();
return {
...defaultResponse,
name: body.name,
};
});No-body Procedures
Procedures without an output schema respond with an empty body:
server.mock('deleteUser', undefined);Default RPC Routing
Procedures that don't specify .route({ path }) get a path derived from
their key in the contract (e.g. planet.find → /planet/find) with the
default method POST, matching oRPC's built-in RPC routing behaviour.
tRPC
Static Responses
Override a procedure by its key path in the router:
server.mock('getUser', { id: '1', name: 'Alice' });
server.mock('post.byId', { id: '1', title: 'Hello', body: 'World' });Callback Resolvers
For dynamic responses, pass a callback with access to the request and a pre-generated default:
server.mock('createUser', async ({ request }, defaultResponse) => {
const body = await request.json();
return {
...defaultResponse,
name: body.name,
};
});Output Schemas and Auto-generation
tRPC procedures that define .output(zodSchema) get auto-generated mock
responses, just like ts-rest and oRPC. Procedures that rely on TypeScript
inference from the handler's return type (no .output() call) respond with
an empty body by default — override them with mock():
// Procedure defined with .output() → auto-generated response ✓
// Procedure without .output() → empty body, override with:
server.mock('health', { status: 'ok' });HTTP Method Mapping
tRPC queries are intercepted as GET requests and mutations as POST,
matching tRPC's HTTP transport. Procedure paths use dot notation for nested
routers (e.g. post.create → ${host}/post.create).
Shared
Custom MSW Handlers
Both adapters expose the underlying MSW primitives for routes outside your contract:
server.use(
server.msw.http.get('http://localhost:3000/health', () => {
return new Response('ok');
}),
);Request Validation
Request bodies for mutation routes (POST, PUT, PATCH) are automatically
validated against the contract's Zod schema (ts-rest and oRPC). A ZodError
is thrown if the request body doesn't match, which helps catch test setup
mistakes. tRPC procedures skip body validation since tRPC queries use query
params and mutations use a wrapped body format.
API
mockTsRest(host, ...contracts)
Creates and starts a mock server for ts-rest contracts.
| Parameter | Type | Description |
| -------------- | ------------- | ------------------------------------------------------- |
| host | string | Base URL to intercept (e.g. 'http://localhost:3000'). |
| ...contracts | AppRouter[] | One or more ts-rest router contracts. |
Returns an object with:
| Property / Method | Description |
| ------------------------------------------ | ------------------------------------------------------ |
| get(path, status, resolver, options?) | Override a GET route. |
| post(path, status, resolver, options?) | Override a POST route. |
| put(path, status, resolver, options?) | Override a PUT route. |
| patch(path, status, resolver, options?) | Override a PATCH route. |
| delete(path, status, resolver, options?) | Override a DELETE route. |
| use(...handlers) | Register additional MSW request handlers. |
| close() | Stop the mock server and clean up. |
| msw.server | The underlying MSW SetupServerApi instance. |
| msw.http | The MSW http namespace for building custom handlers. |
mockOrpc(host, ...contracts)
Creates and starts a mock server for oRPC contracts.
| Parameter | Type | Description |
| -------------- | ------------------ | ------------------------------------------------------- |
| host | string | Base URL to intercept (e.g. 'http://localhost:3000'). |
| ...contracts | OrpcRouterLike[] | One or more oRPC contract routers. |
Returns an object with:
| Property / Method | Description |
| ------------------------------- | ------------------------------------------------------ |
| mock(key, resolver, options?) | Override a procedure by its dotted key path. |
| use(...handlers) | Register additional MSW request handlers. |
| close() | Stop the mock server and clean up. |
| msw.server | The underlying MSW SetupServerApi instance. |
| msw.http | The MSW http namespace for building custom handlers. |
mockTrpc(host, ...routers)
Creates and starts a mock server for tRPC routers.
| Parameter | Type | Description |
| ------------ | ------------------ | -------------------------------------------------------------------------------------------- |
| host | string | Base URL to intercept, including any tRPC path prefix (e.g. 'http://localhost:3000/trpc'). |
| ...routers | TrpcRouterLike[] | One or more tRPC routers created with t.router(). |
Returns an object with:
| Property / Method | Description |
| ------------------------------- | ----------------------------------------------------------------------------- |
| mock(key, resolver, options?) | Override a procedure by its dotted key path. Queries → GET, mutations → POST. |
| use(...handlers) | Register additional MSW request handlers. |
| close() | Stop the mock server and clean up. |
| msw.server | The underlying MSW SetupServerApi instance. |
| msw.http | The MSW http namespace for building custom handlers. |
Note: Procedures with
.output(zodSchema)get auto-generated defaults. Procedures without.output()respond with an empty body until overridden viamock().
Resolver
Each override helper accepts a resolver which is either:
- A static value matching the response schema for the given endpoint, or
- A callback
(info, defaultResponse) => responsewhere:infois the MSW resolver info with a typedrequest.json().defaultResponseis a randomly generated value that satisfies the schema.- The return value can be a
Promise.
Architecture
The codebase is split into a generic layer and framework-specific adapters:
| Module | Responsibility |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| mockHttpApi.ts | Framework-agnostic MSW + Zod plumbing: server lifecycle, default handler creation, override registration, mock data generation, and shared types/errors. |
| mockTsRest.ts | ts-rest adapter: contract-walking, type generics (GetPath, FilterRoute, GetResponseBody, etc.), and the mockTsRest() entry point. Uses HTTP method + path for overrides. |
| mockOrpc.ts | oRPC adapter: contract-walking via the ~orpc property, type generics (ProcedureKeys, InferOrpcOutput, etc.), and the mockOrpc() entry point. Uses dotted procedure keys for overrides. |
| mockTrpc.ts | tRPC adapter: router-record walking via _def.record, type generics (TrpcProcedureKeys, InferTrpcOutput, etc.), and the mockTrpc() entry point. Uses dotted procedure keys for overrides. Queries → GET, mutations → POST. |
To add support for a new contract library, create a new adapter that imports
from mockHttpApi and adds framework-specific type inference and route
extraction. The generic layer handles all MSW interaction.
Key exports from mockHttpApi
| Export | Purpose |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| createMockServer(handlers) | Starts an MSW server with default handlers; returns { msw, use, close }. |
| createDefaultHandler(host, method, path, status, schema) | Creates a single default MSW handler for a route. null schema = no body. |
| registerOverride(server, host, fn, path, status, schema, resolver, validator, opts) | Registers an override handler — the runtime core of every HTTP-method helper. |
| mockSchema(schema) | Generates random data conforming to a Zod schema. |
| Method, MutationMethod, GetResolver | Shared types reused by every adapter. |
