| name | build-simulation |
| description | Build a fully simulated API from an OpenAPI spec using Counterfact. Generate the initial scaffold, research the real API's behaviour, implement stateful context classes, write tests for those classes, and configure scenario scripts (including a startup scenario) so the mock server is immediately useful without manual setup.
|
| applyTo | ["**/*.{yaml,yml,json}","**/routes/**/*.{ts,js}","**/*context.{ts,js}","**/scenarios/**/*.{ts,js}"] |
Build-Simulation Skill
Purpose
Guide an AI agent through the full workflow of turning a bare OpenAPI spec into
a realistic, stateful API simulation using Counterfact. This skill covers every
step: generation, research, context implementation, testing, and scenario setup.
Step 1 — Generate the initial scaffold
Run Counterfact against the spec to produce a working server immediately:
npx counterfact@latest <spec-url-or-path> <output-directory>
Example:
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
This creates:
<output-directory>/
├── routes/ # one .ts file per API path — edit these
└── types/ # generated request/response types — never edit these
Every endpoint is live right away. By default each handler returns
schema-valid random data (.random()). The simulation work that follows will
replace those random responses with realistic, stateful behaviour.
Docs: Getting Started
Step 2 — Research the real API
Because the spec describes a well-known public API, look up official
documentation before writing any simulation code. Understanding what the real
API actually does is what separates a useful simulation from a pretty type stub.
Do at least the following:
- Read the official API docs — authentication flows, pagination styles,
required vs. optional fields, status codes, error shapes, and any notable
business rules (e.g. "a pet cannot be ordered if its status is
sold").
- Check for public examples and SDKs — official SDKs often clarify
which fields are commonly populated and how resources relate to each other.
- Note realistic data shapes — names, IDs, statuses, and dates that
appear in the real API's example responses will make the simulation
convincing.
Use this research to inform what methods the context classes should expose,
what validation the context should perform, and what data the startup scenario
should seed.
Step 3 — Implement context classes
Handlers should be thin. All stateful logic — lookups, mutations, derived
data, invariants, error conditions — belongs in a _.context.ts file. Handlers
read from context and return responses; they contain no business logic of
their own.
Single-domain API
Place one _.context.ts at the root of the routes tree:
import type { Pet } from "../types/components/pet.types.js";
export class Context {
private pets = new Map<number, Pet>();
private nextId = 1;
add(data: Omit<Pet, "id">): Pet {
const pet = { ...data, id: this.nextId++ };
this.pets.set(pet.id, pet);
return pet;
}
get(id: number): Pet | undefined {
return this.pets.get(id);
}
list(status?: Pet["status"]): Pet[] {
const all = [...this.pets.values()];
return status ? all.filter((p) => p.status === status) : all;
}
remove(id: number): boolean {
return this.pets.delete(id);
}
}
Then wire the context into each handler:
export const GET: HTTP_GET = ($) =>
$.response[200].json($.context.list($.query.status));
export const POST: HTTP_POST = ($) =>
$.response[200].json($.context.add($.body));
export const GET: HTTP_GET = ($) => {
const pet = $.context.get($.path.petId);
return pet
? $.response[200].json(pet)
: $.response[404].text(`Pet ${$.path.petId} not found`);
};
export const DELETE: HTTP_DELETE = ($) => {
$.context.remove($.path.petId);
return $.response[200];
};
Multi-domain API
For APIs with several distinct domains (users, orders, payments, …), give each
domain its own _.context.ts at its subtree boundary. Use
$.loadContext(path) to reach across boundaries when one domain needs data
from another:
import type { Context as UsersContext } from "../../users/_.context.js";
export const GET: HTTP_GET = ($) => {
const payment = $.context.getById($.path.paymentId);
if (!payment) return $.response[404].text("Payment not found");
const usersCtx = $.loadContext("/users") as UsersContext;
const user = usersCtx.getById(payment.userId);
return $.response[200].json({ ...payment, user });
};
Keep each context class focused on a single responsibility. If a class is
growing too large, split it along domain lines.
Docs:
Step 4 — Test the context classes
The context class accumulates real logic; bugs in it break every handler that
depends on it. Write unit tests that exercise the class directly, with no
server, no $ object, and no HTTP machinery.
import { Context } from "../api/routes/_.context.js";
describe("Context", () => {
let context: Context;
beforeEach(() => {
context = new Context();
});
it("assigns sequential ids", () => {
const a = context.add({ name: "Fluffy", status: "available", photoUrls: [] });
const b = context.add({ name: "Rex", status: "available", photoUrls: [] });
expect(a.id).toBe(1);
expect(b.id).toBe(2);
});
it("returns undefined for an unknown id", () => {
expect(context.get(99)).toBeUndefined();
});
it("filters by status", () => {
context.add({ name: "Fluffy", status: "available", photoUrls: [] });
context.add({ name: "Rex", status: "sold", photoUrls: [] });
expect(context.list("sold")).toHaveLength(1);
});
it("removes a pet", () => {
const pet = context.add({ name: "Fluffy", status: "available", photoUrls: [] });
context.remove(pet.id);
expect(context.get(pet.id)).toBeUndefined();
});
});
Do not test handler files. Handlers are intentionally thin, freely
editable, and contain no logic worth automating against. Only the context class
needs tests.
Docs: Pattern: Test the Context, Not the Handlers
Step 5 — Set up scenario scripts
Scenario scripts are TypeScript files in the scenarios/ directory. Each
file exports named functions that receive a $ argument giving access to live
context and a route builder. Run them from the REPL with .scenario, or have
one run automatically at startup.
The startup scenario
Export a function named startup from scenarios/index.ts. Counterfact calls
it automatically when the server initialises, right before the REPL prompt
appears. Use it to seed realistic data so the server is immediately useful
without any manual REPL commands.
import type { Scenario } from "../types/_.context.js";
export const startup: Scenario = ($) => {
$.context.add({ name: "Fluffy", status: "available", photoUrls: [] });
$.context.add({ name: "Rex", status: "sold", photoUrls: [] });
};
For large startup sets, delegate to helper functions in separate files so each
file stays focused and the helpers can also be called from the REPL:
import type { Scenario } from "../types/_.context.js";
import { addPets } from "./pets.js";
import { addOrders } from "./orders.js";
export const startup: Scenario = ($) => {
addPets($, 20, "dog");
addOrders($, 5);
};
import type { Scenario$ } from "../types/_.context.js";
export function addPets($: Scenario$, count: number, species: string) {
for (let i = 0; i < count; i++) {
$.context.add({
name: `${species} ${i + 1}`,
status: "available",
photoUrls: [],
});
}
}
Additional scenario scripts
Write named exports for other useful states — an empty store, a rate-limited
service, a full inventory. Run them from the REPL whenever you need them:
export const soldOut: Scenario = ($) => {
$.context.list().forEach((p) => {
p.status = "sold";
});
};
⬣> .scenario soldOut
Applied soldOut
Docs:
Step 6 — Add failure and edge-case scenarios
Encode failure conditions in the context and toggle them on demand. This lets
consumers of the simulation test error-handling code without coordinating with
the real API.
export class Context {
isRateLimited = false;
isDown = false;
}
export const GET: HTTP_GET = ($) => {
if ($.context.isDown) return $.response[500].text("Service unavailable");
if ($.context.isRateLimited) return $.response[429].text("Too many requests");
const pet = $.context.get($.path.petId);
return pet ? $.response[200].json(pet) : $.response[404].text("Not found");
};
Toggle from the REPL or via a scenario:
⬣> context.isRateLimited = true
⬣> client.get("/pet/1")
{ status: 429, body: 'Too many requests' }
Docs: Pattern: Simulate Failures and Edge Cases
Quick-reference: key documentation
Checklist
Work through these steps in order: