npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@archon-research/http-client-msw

v0.11.0

Published

Typed [msw](https://mswjs.io) mocks for `@archon-research/http-client-core`.

Downloads

1,285

Readme

@archon-research/http-client-msw

Typed msw mocks for @archon-research/http-client-core.

The generated OpenAPI paths type is the endpoint definition. The same type that drives createApiClient and createQueryApi drives the mock handlers, so a handler path, its params, and the body it answers with are checked against the contract — a fixture that no longer matches the API fails to typecheck instead of quietly making every test that reads it a false pass. See DESIGN.md for the contract, the openapi-msw verdict, and the deliberate limits.

Installation

npm install -D @archon-research/http-client-msw msw

msw is a peer dependency: the service worker script is generated from the installed msw and version-checked at start, so the app has to own the version.

Generate the worker script once and commit it:

npx msw init public --save

public/mockServiceWorker.js is a build artifact that belongs in git — the app serves it, and --save records the path in package.json so msw warns when an upgrade leaves it stale. Adjust public if the bundler serves static files from elsewhere.

Usage

1. Generate types

npx uikit-openapi-generate --schema openapi.json --output src/api.types.ts

The same src/api.types.ts the client and query layer already use. Nothing about the mocks is described twice.

2. Write the handlers

Keep them in one module — a src/mocks/ folder in the app, or a workspace package (@your-scope/api-mocks) when a Playwright suite and a unit-test suite both consume them:

// src/mocks/index.ts
import {
  createMockApi,
  createMockStore,
  createSeededRng,
  mockDelay,
  setupMocks,
} from '@archon-research/http-client-msw';

import type { paths } from '../api.types';

// The same baseUrl the app passes to `createApiClient`.
const mock = createMockApi<paths>({ baseUrl: '/api' });

const rng = createSeededRng(1337);
const positions = createMockStore(() =>
  Array.from({ length: 8 }, (_, index) => ({
    id: `p${index + 1}`,
    label: `Position ${index + 1}`,
    health: rng.int(50, 200) / 100,
  })),
);

export const mocks = setupMocks(
  [
    mock.get('/positions', async ({ query, response }) => {
      await mockDelay(300);
      const limit = Number(query.get('limit') ?? '25');

      return response(200).json(positions.list().slice(0, limit));
    }),

    mock.get('/positions/{id}', ({ params, response }) => {
      const position = positions.get(params.id);

      // Both branches are checked: 200 takes a Position, 404 takes the
      // operation's own error body.
      return position
        ? response(200).json(position)
        : response(404).json({ message: `no position ${params.id}` });
    }),

    mock.post('/positions/{id}/close', ({ params, response }) =>
      positions.remove(params.id)
        ? response(204).empty()
        : response(404).json({ message: 'already closed' }),
    ),
  ],
  // Everything a handler writes to is restored by `reset()`. Order matters:
  // callbacks run as declared, and the store's seed function draws from the rng,
  // so the rng has to be rewound first or each reset re-seeds from a different
  // point in the sequence.
  { onReset: [rng.reset, positions.reset] },
);

For a route the OpenAPI document does not describe at all — an auth callback on another host, say — mock.untyped is msw's own http object, and response.untyped(new Response(...)) returns an arbitrary response from a typed handler.

3. Run them in the browser (dev, and Playwright)

Start the worker before rendering, so no component can fire a request the worker is not yet intercepting:

// src/main.tsx
import { createRoot } from 'react-dom/client';

import { App } from './App';

if (import.meta.env.VITE_API_MOCKS === '1') {
  const { setupMockWorker } = await import(
    '@archon-research/http-client-msw/browser'
  );
  const { mocks } = await import('./mocks');

  // `baseUrl` here is the app's public base path, not the API base: it locates
  // `mockServiceWorker.js` for a subpath deployment.
  const mockWorker = setupMockWorker(mocks, {
    baseUrl: import.meta.env.BASE_URL,
  });

  // Reachable from a Playwright test; see below.
  Object.assign(window, { resetMocks: () => mockWorker.reset() });

  await mockWorker.start();
}

createRoot(document.getElementById('root')!).render(<App />);
VITE_API_MOCKS=1 npm run dev

The gate matters as much as the mocks. Vite replaces import.meta.env.VITE_* statically, so with the flag unset the whole branch is dead code and neither msw nor the fixtures reach a production bundle — which is only true because both imports are dynamic and behind the check. A runtime process.env read or a static import of the mocks module would bundle them either way.

start() is idempotent, so a hot reload or a test fixture may call it again without re-registering the worker. stop() releases that, so a later start() registers again rather than resolving into a stopped worker.

For a Playwright suite, serve the app with the same flag on — the browser worker answers from the same handlers — and reset between tests through the hook the entry exposed:

// tests/positions.spec.ts
declare global {
  interface Window {
    resetMocks?: () => void;
  }
}

test.beforeEach(async ({ page }) => {
  await page.goto('/');
  await page.evaluate(() => window.resetMocks?.());
});

A full reload re-runs the entry and re-seeds the stores anyway; the explicit resetMocks() matters when a test navigates within the app instead.

4. Run them in vitest

Same handler array, node interceptors:

// src/test-setup.ts
import { setupMockServer } from '@archon-research/http-client-msw/node';
import { afterAll, afterEach, beforeAll } from 'vitest';

import { mocks } from './mocks';

const mockServer = setupMockServer(mocks);

// `onUnhandledRequest` defaults to 'error': an unmocked request in a suite is a
// hole in the fixtures, not something to warn about and scroll past.
beforeAll(() => mockServer.listen());
afterEach(() => mockServer.reset());
afterAll(() => mockServer.close());
// vitest.config.ts
export default defineConfig({
  test: { setupFiles: ['./src/test-setup.ts'] },
});

reset() covers both kinds of leakage between tests: state a handler wrote, and handlers a test installed with mockServer.server.use(...).

An origin-relative baseUrl works in both environments because handler paths are matched on any origin by default — msw would otherwise leave a relative path unmatched under setupServer, where every request URL is absolute. Pass origin: 'exact' to createMockApi to opt out; see DESIGN.md.

API surface

| Export | What it does | | --- | --- | | createMockApi<TPaths>(options?) | Typed handler factories per method, plus untyped | | setupMocks(handlers, options?) | Bundles handlers with their state resets, environment-neutral | | setupMockWorker(mocks, options?) | /browser. Serves them from a service worker; idempotent start() | | setupMockServer(mocks) | /node. Serves them from msw's node interceptors | | createMockStore(seedFn, options?) | In-memory collection so a write shows up in the next read | | createSeededRng(seed) | Deterministic PRNG for reproducible generated fixtures | | mockDelay(ms \| { test, dev }) | Env-aware latency; no delay under test by default | | resolveMockDelay / isTestEnvironment | The delay decision, for a consumer's own helpers | | resolveWorkerScriptUrl / normalizeApiBaseUrl / resolveHandlerBase / isAbsoluteUrl | The URL primitives | | buildWorkerStartOptions / createIdempotentStart | /browser. The start decisions, unit-testable |

Handler and resolver types are re-exported as MockHandler, MockResponseResolver, MockPathsFor, MockRequestBodyFor, and MockResponseBodyFor, so a helper written around a resolver types against the same msw copy this package resolves.

Not in v1

Handlers generated from the OpenAPI document, runtime request-body validation, named scenario switching, GraphQL/websocket typing, per-test handler isolation via msw's boundary, and fault injection are deliberately out of scope — see DESIGN.md.

Peer dependencies

  • msw (^2.10.5)

See also