v0.1.0 · seeded, spec-driven

API mocks that agree with each other.

Point it at an OpenAPI 3.x file and get consistent, seeded, scenario-driven responses. The same customer appears everywhere, pages add up, and “what if it returns 429?” is one word away.

npm install --save-dev m2mockforge
openapi.yamlseed "demo"consistent
  • 200GET /orders?customerId=3

    [{"id":10,"customerId":3,"status":"pending","total":311.91}]

  • 200GET /customers/3/orders

    [{"id":10,"customerId":3,"status":"pending","total":311.91}]

Real output from forge.handle(). Same orders from two different routes.

1runtime dependency: yaml
Node ≥18.18ESM, ships its own types
15built-in scenarios
3entry points: core, MSW, Node

Consistency

Most mocks answer each request alone. These share a world.

It is not another request interceptor. It decides what the response is, and plugs into the tool that catches the request.

Related

Endpoints agree.

A customerId on an order points at the same customer an embedded customer resolves to, through any chain.

/orders?customerId=3 = /customers/3/orders

Seeded

Reproducible, by design.

Collections are the first collectionSize ids, and pages slice the same ordered list, so they never overlap and always sum to the total.

Nothing is stored between tests.

Scenarios

Failure is a setting.

Rate limits, server errors, timeouts, empties and boundaries, without editing the spec or the data.

forge.useScenario("rate-limit")

What you get

Realistic enough to test against, small enough to trust.

  • A world behind the spec

    Every record is a pure function of (seed, entity, id). The same customer shows up in the customer, order and payment endpoints, and /orders?customerId=3 equals /customers/3/orders.

  • Seeded and stateless

    Same seed, same data. Nothing is stored, so there is no state to leak between tests. Opt into stateful for create, update and delete.

  • Scenarios are first-class

    429, 500, 503, timeouts, network errors, empty pages, schema boundaries and slow responses, selected per call, per header or per forge.

  • Plugs into MSW

    m2mockforge decides what the response is. MSW, or the built-in Node server, decides how requests are caught.

  • Specs are data

    No eval, no external $refs, capped YAML aliases and screened patterns. Generated data is synthetic: example.com, mock tokens, 555-01xx phones.

  • Browser-safe core, Node extras

    The core has no Node built-ins and a test enforces it. The Node server and the CLI sit behind separate entry points. One runtime dependency, yaml.

Workflow

Spec in. World built. Scenario on top.

Schemas don't define business rules, so a generated order total won't equal the sum of its lines. Use overrides or a handler for the invariants that matter.

Spec

Load an OpenAPI 3.x document. It is parsed as data and validated with diagnostics.

  1. 1
    createMockForge({ spec })

World

Entities and relationships are derived once, then every response is generated from them.

  1. 2
    forge.handle(request)

Scenario

Flip the behaviour without touching the data: failures, delays, empties, boundaries.

  1. 3
    forge.useScenario(name)

Read what it doesn't support →

How it works

From a function call, to MSW, to a server.

  1. 1

    Call it directly

    Hand it a request, get a response and an explanation of why it looks that way.

    ts
    import { readFileSync } from "node:fs";
    import { createMockForge } from "m2mockforge";
    
    const forge = createMockForge({
      spec: readFileSync("openapi.yaml", "utf8"), // or an already-parsed object
      seed: "my-test-suite",                      // same seed => same data, every run
    });
    
    const res = await forge.handle({ method: "GET", url: "/customers/7", headers: { authorization: "Bearer x" } });
    res.status; // 200
    res.body;   // a Customer that satisfies your schema; customer 7 is the same everywhere
    console.log(forge.explain(res)); // why the response looks the way it does
  2. 2

    Plug it into MSW

    Works in the browser and in Node. Faults map to MSW's own error and delay.

    ts
    import { setupServer } from "msw/node";
    import { createMockForge } from "m2mockforge";
    import { createMswHandlers } from "m2mockforge/msw";
    
    declare const spec: string;
    
    const forge = createMockForge({ spec, seed: 1 });
    const server = setupServer(...createMswHandlers(forge, { baseUrl: "https://api.example.com" }));
    server.listen();
    
    forge.useScenario("rate-limit"); // every call now returns 429 + Retry-After
    forge.useScenario("network-error"); // fetch() rejects
    forge.clearScenario();
  3. 3

    Or run it as a server

    Binds to 127.0.0.1 by default. Clients pick a scenario per request with a header.

    sh
    npx m2mockforge validate openapi.yaml
    npx m2mockforge routes openapi.yaml
    npx m2mockforge generate openapi.yaml GET /customers/7 --header "Authorization: Bearer x" --seed 42
    npx m2mockforge serve openapi.yaml --port 4010 --scenario rate-limit
    ts
    import { createMockForge } from "m2mockforge";
    
    declare const spec: string;
    
    createMockForge({
      spec,
      overrides: { Customer: { currency: "EUR" } }, // always-on entity data
      scenarios: [
        { name: "vip", overrides: { Customer: { tier: "pro" } } }, // data scenario
        { name: "payments-down", match: ["getPayment", "POST /orders"], status: 502,
          body: { error: { code: "GATEWAY", message: "down" } } }, // operation scenario
        { name: "flaky", base: "server-error", after: 1, times: 2 }, // OK, fail, fail, OK, OK...
      ],
    });

Playground

Edit the spec. Send requests. Switch the scenario.

This runs the m2mockforge core from this repository in your browser, on the OpenAPI document below. Nothing is mocked and nothing leaves the page.

2 requestsscenario success · seed demo
  • 200GET /orders?customerId=3
    [
      {
        "id": 10,
        "customerId": 3,
        "status": "pending",
        "total": 311.91
      }
    ]
  • 200GET /customers/3/orders
    [
      {
        "id": 10,
        "customerId": 3,
        "status": "pending",
        "total": 311.91
      }
    ]

Both requests return the same orders: relationships are a pure function of the owning record, so a nested route and a query filter can never disagree. Each order's customerId matches the customer in the path. How consistency works →

The code behind this panel
ts
import { createMockForge, type MockResponse } from "m2mockforge";

export type Input = {
  /** An OpenAPI 3.x document as YAML or JSON text. */
  spec: string;
  seed: string;
  scenario: string;
  /** One request per line, such as `GET /orders?customerId=3`. */
  requests: string;
};

export type Call = { line: string; response: MockResponse; why: string };

/** The same engine the CLI, the MSW adapter and the Node server use. No network, no files. */
export async function run({ spec, seed, scenario, requests }: Input) {
  const forge = createMockForge({ spec, seed, collectionSize: 10, enforceAuth: false });
  if (scenario !== "success") forge.useScenario(scenario);

  const calls: Call[] = [];
  for (const line of requests.split("\n").map((l) => l.trim()).filter(Boolean)) {
    const [method = "GET", url = "/"] = line.split(/\s+/);
    const response = await forge.handle({ method, url });
    calls.push({ line, response, why: forge.explain(response) });
  }
  return { calls, diagnostics: forge.formatDiagnostics(), scenarios: forge.scenarioNames() };
}

export type Run = Awaited<ReturnType<typeof run>>;

Options

A few knobs cover the rest.

Config files for the CLI are data and are never executed. Scenarios written in code can also carry handlers.

  • seedNumber or string. Drives all generation: same seed, same data.
  • scenarioNamed behaviour, active by default or chosen per call or per x-m2mockforge-scenario header.
  • overridesAlways-on entity data by component name: an object, or a function of the record.
  • handlersCustom behaviour by operationId or METHOD /path. Return undefined to fall through.
  • statefulPersist POST, PUT, PATCH and DELETE in memory for one forge instance.
  • limitsBounds on spec size, operations, schema nodes, depth and collection size. Exceeding one throws.

See every option →

Documentation

Everything is written down.

More from M2Toolkit