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
v0.1.0 · seeded, spec-driven
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 m2mockforgeGET /orders?customerId=3[{"id":10,"customerId":3,"status":"pending","total":311.91}]
GET /customers/3/orders[{"id":10,"customerId":3,"status":"pending","total":311.91}]
Real output from forge.handle(). Same orders from two different routes.
Consistency
It is not another request interceptor. It decides what the response is, and plugs into the tool that catches the request.
A customerId on an order points at the same customer an embedded customer resolves to, through any chain.
/orders?customerId=3 = /customers/3/orders
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.
Rate limits, server errors, timeouts, empties and boundaries, without editing the spec or the data.
forge.useScenario("rate-limit")
What you get
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.
Same seed, same data. Nothing is stored, so there is no state to leak between tests. Opt into stateful for create, update and delete.
429, 500, 503, timeouts, network errors, empty pages, schema boundaries and slow responses, selected per call, per header or per forge.
m2mockforge decides what the response is. MSW, or the built-in Node server, decides how requests are caught.
No eval, no external $refs, capped YAML aliases and screened patterns. Generated data is synthetic: example.com, mock tokens, 555-01xx phones.
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
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.
Load an OpenAPI 3.x document. It is parsed as data and validated with diagnostics.
createMockForge({ spec })Entities and relationships are derived once, then every response is generated from them.
forge.handle(request)Flip the behaviour without touching the data: failures, delays, empties, boundaries.
forge.useScenario(name)How it works
Hand it a request, get a response and an explanation of why it looks that way.
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 doesWorks in the browser and in Node. Faults map to MSW's own error and delay.
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();Binds to 127.0.0.1 by default. Clients pick a scenario per request with a header.
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-limitimport { 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
This runs the m2mockforge core from this repository in your browser, on the OpenAPI document below. Nothing is mocked and nothing leaves the page.
GET /orders?customerId=3[
{
"id": 10,
"customerId": 3,
"status": "pending",
"total": 311.91
}
]GET /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 →
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
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.Documentation
More from M2Toolkit