# Serve fixtures from Mock Service Worker

Write one fixture recipe and use it in three places: a unit test builds it
directly, a [Mock Service Worker](https://mswjs.io/) (MSW) handler returns it from
a mocked endpoint, and a Storybook story previews it. Every call builds new
objects, so one test or story cannot change the data another one sees. The same
id always gives the same data, so a failure in one place can be reproduced in the
others.

The recipe uses Zod for the response schema, `@mimlet/consumers` for the JSON
response and MSW 3.0.2:

```sh
npm install --save-dev @mimlet/core@0.1.0-beta.3 @mimlet/consumers@0.1.0-beta.3 @mimlet/zod@0.1.0-beta.3 zod@4.6.5 msw@3.0.2
```

Any Mimlet builder works the same way. Use your own adapter if your API schema
comes from TypeBox, Valibot, ArkType, JSON Schema or OpenAPI.

## 1. Write the fixture recipe

```ts
import { z } from 'zod';
import { createSession } from '@mimlet/core';
import { fromZodFactory } from '@mimlet/zod';

// The JSON your API sends. Validated output is the response body.
export const Order = z.object({
  id: z.string(),
  status: z.enum(['open', 'paid', 'cancelled']),
  customer: z.object({ name: z.string(), email: z.email() }),
  lines: z
    .array(z.object({ sku: z.string(), quantity: z.int().min(1), unitPriceCents: z.int().min(0) }))
    .min(1),
  totalCents: z.int().min(0),
});

const products = [
  { sku: 'MUG', unitPriceCents: 1200 },
  { sku: 'TEE', unitPriceCents: 2500 },
  { sku: 'CAP', unitPriceCents: 1800 },
];

// The id seeds the session: the same id always gives the same order, and
// every build returns new objects. The total is a transform, which runs
// after with(), so an order with changed lines keeps a matching total.
export const orders = fromZodFactory(Order, (id: string) => {
  const session = createSession({ seed: id, fingerprint: 'orders/v1', provider: 'shop-mocks@1' });
  return {
    id,
    status: 'open',
    customer: {
      name: session.pick(['Ada Lovelace', 'Grace Hopper', 'Alan Turing']),
      email: `${id}@example.test`,
    },
    lines: Array.from({ length: session.integer(1, 3) }, () => ({
      ...session.pick(products),
      quantity: session.integer(1, 4),
    })),
    totalCents: 0,
  };
}).transform((order) => ({
  ...order,
  totalCents: order.lines.reduce((sum, line) => sum + line.quantity * line.unitPriceCents, 0),
}));
```

[View the tested fixture recipe](https://github.com/JeffreyNijs/mimlet/blob/main/examples/recipes/msw-orders.ts).

The factory takes the order id and seeds a session from it. A request for
`order-1`, a unit test that builds `order-1` and a story that shows `order-1` all
get the same order, and each one gets its own copy. Export the builder, not a
built object: a shared constant would let one test's change leak into the next.

`buildValidated()` checks every order against the schema, so the mock cannot
drift from the API contract without a test noticing. Here the schema's input and
output are the same JSON. If your schema transforms values when it parses (for
example a date string into a `Date`), serve the input from `build()` instead,
because that is what goes over the wire.

## 2. Return it from an MSW handler

```ts
import { http } from 'msw';
import { jsonResponseResolver } from '@mimlet/consumers';
import { orders } from './msw-orders.js';

export const api = 'https://shop.example.test/api';

// Every request builds a new order and gets its own JSON response.
// Pass a builder variant to change what the endpoint returns.
export function orderHandler(variant = orders) {
  return http.get<{ id: string }>(`${api}/orders/:id`, ({ request, params }) =>
    jsonResponseResolver(() => variant.buildValidated(params.id))(request)
  );
}

export const handlers = [orderHandler()];
```

[View the tested handlers](https://github.com/JeffreyNijs/mimlet/blob/main/examples/recipes/msw-handlers.ts).

`jsonResponseResolver` from `@mimlet/consumers` turns the build into a native
`Response`: a new JSON body for each request, a JSON `content-type` header and a
1 MiB body limit. It passes request aborts through and never turns an error into a
successful response. If a builder variant fails validation, `buildValidated()`
throws, MSW answers with status 500 and logs the error, and the test fails instead
of reading a bad body. Mimlet does not intercept requests or replace `fetch`: MSW
does, in the test setup you control.

## 3. Use the same recipe in tests

```ts
import assert from 'node:assert/strict';
import { after, afterEach, before, test } from 'node:test';
import { setupServer } from 'msw/node';
import { api, handlers, orderHandler } from './msw-handlers.js';
import { Order, orders } from './msw-orders.js';

const server = setupServer(...handlers);
before(() => server.listen({ onUnhandledFrame: 'error' }));
afterEach(() => server.resetHandlers());
after(() => server.close());

// The code under test: an API client that fetches and parses an order.
async function getOrder(id: string) {
  const response = await fetch(`${api}/orders/${id}`);
  if (!response.ok) {
    throw new Error(`GET order failed with ${response.status}`);
  }
  return Order.parse(await response.json());
}

test('a unit test builds the order it needs', () => {
  const twoMugs = [{ sku: 'MUG', quantity: 2, unitPriceCents: 1200 }];
  const order = orders.with({ lines: twoMugs }).buildValidated('order-1');
  assert.equal(order.totalCents, 2400);
});

test('every build returns new objects', () => {
  const order = orders.buildValidated('order-1');
  order.lines.length = 0;
  assert.notEqual(orders.buildValidated('order-1').lines.length, 0);
});

test('the MSW handler serves the order the unit test builds', async () => {
  assert.deepEqual(await getOrder('order-1'), orders.buildValidated('order-1'));
});

test('one test changes the response with a builder variant', async () => {
  server.use(orderHandler(orders.with({ status: 'cancelled' })));
  assert.equal((await getOrder('order-2')).status, 'cancelled');
});
```

[View the tested test file](https://github.com/JeffreyNijs/mimlet/blob/main/examples/recipes/msw-test.ts).

The file runs with `node --test`. With Vitest or Jest, use `beforeAll`,
`afterEach` and `afterAll` for the same three server calls. MSW 3 renamed the
`onUnhandledRequest` option to `onUnhandledFrame`; on MSW 2, pass
`onUnhandledRequest: 'error'` instead.

- A unit test builds the order it needs with `with()` and never starts MSW.
- A test of code that fetches gets the same order through the handler. Comparing
  the response with a direct build shows that both paths use one recipe.
- `server.use(orderHandler(variant))` changes the response for one test, and
  `server.resetHandlers()` restores the default after it.

## 4. Preview it in Storybook

```ts
import { fixtureLoader } from '@mimlet/consumers';
import { handlers } from './msw-handlers.js';
import { orders } from './msw-orders.js';

// A Storybook CSF file, without the component import and render functions.
// msw-storybook-addon serves parameters.msw.handlers to components that fetch.
export default {
  title: 'Orders/OrderPage',
  parameters: { msw: { handlers } },
};

// A component that fetches order-1 gets it from the handlers above.
export const Fetched = { args: { orderId: 'order-1' } };

// A component that takes the order as a prop gets a fresh copy from a loader.
export const Loaded = {
  loaders: [fixtureLoader('order', () => orders.buildValidated('order-1'))],
};
```

[View the tested story file](https://github.com/JeffreyNijs/mimlet/blob/main/examples/recipes/msw-story.ts).

With [msw-storybook-addon](https://github.com/mswjs/msw-storybook-addon) set up in
your Storybook preview, `parameters.msw.handlers` serves the same handlers to
components that fetch. A component that takes the order as a prop can use
`fixtureLoader` instead: Storybook passes the value as `loaded.order`, and the
loader builds and clones a new order each time Storybook runs it, so a component
that mutates its props cannot change the next story.

The recipe leaves out the component import and the render function, because
Storybook and a UI framework are not installed in this repository's tests. The
tests check the story's loader and handlers, not a rendered story.

## Fresh and reproducible data

- **New objects for every call.** Each build runs the factory again, Zod's parser
  returns new objects, each response gets its own body, and `fixtureLoader` clones
  the value. Nothing is cached between calls.
- **Same id, same data.** The id is the seed, so the data does not depend on the
  order in which tests run or requests arrive. This holds for the same recipe
  code and package versions: editing the factory, for example adding a `pick()`
  call, can change the data for every id.
- **Variants stay coherent.** `with()` replaces the fields a test cares about, and
  the transform recomputes the total from whatever lines the order ends up with.

## How this is tested

`pnpm test:examples` compiles these files against packed Mimlet packages and
MSW 3.0.2 and runs the test file above as written. It also checks that the story
loader returns fresh, equal copies, that the handler returns the same order, that
an invalid variant answers 500 and that an unhandled request fails. Storybook,
msw-storybook-addon and browser service workers are not part of that run.

See the [consumers package](/mimlet/packages/consumers.md) for the full
`jsonResponseResolver` and `fixtureLoader` contracts, and
[seed a database](/mimlet/guide/database-seeding.md) for writing the same kind of fixtures to a
test database.
