Skip to content

msaspence/zod-mock-api

Repository files navigation

api-mock

License: MIT

Type-safe mock servers for ts-rest, oRPC, and tRPC API contracts, powered by MSW and Zod schema generation.

api-mock 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 server and http helper 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 api-mock @anatine/zod-mock @faker-js/faker msw zod

Then 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/server

Note: @ts-rest/core, @orpc/contract, and @trpc/server are 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 'api-mock';

// Per-adapter (tree-shakeable, avoids loading unused adapters)
import { mockTsRest } from 'api-mock/ts-rest';
import { mockOrpc } from 'api-mock/orpc';
import { mockTrpc } from 'api-mock/trpc';

// Generic HTTP layer (for building custom adapters)
import { createMockServer, registerOverride } from 'api-mock/http';

Quick Start

ts-rest

import { initContract } from '@ts-rest/core';
import { mockTsRest } from 'api-mock';
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 'api-mock';
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 'api-mock';
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 via mock().

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) => response where:
    • info is the MSW resolver info with a typed request.json().
    • defaultResponse is 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.

License

MIT

About

A library for automatically mocking ts-rest endpoints based on their Zod schemas

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors