--- 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: ```sh npx counterfact@latest ``` Example: ```sh npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api ``` This creates: ``` / ├── 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](https://counterfact.dev/docs/getting-started.html) --- ## 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: 1. **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`"). 2. **Check for public examples and SDKs** — official SDKs often clarify which fields are commonly populated and how resources relate to each other. 3. **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: ```ts // api/routes/_.context.ts import type { Pet } from "../types/components/pet.types.js"; export class Context { private pets = new Map(); private nextId = 1; add(data: Omit): 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: ```ts // api/routes/pet.ts export const GET: HTTP_GET = ($) => $.response[200].json($.context.list($.query.status)); export const POST: HTTP_POST = ($) => $.response[200].json($.context.add($.body)); ``` ```ts // api/routes/pet/{petId}.ts 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: ```ts // api/routes/payments/{paymentId}.ts 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:** > - [State: Context Objects](https://counterfact.dev/docs/features/state.html) > - [Pattern: Mock APIs with Dummy Data](https://counterfact.dev/docs/patterns/mock-with-dummy-data.html) > - [Pattern: Federated Context Files](https://counterfact.dev/docs/patterns/federated-context.html) --- ## 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. ```ts // test/context.test.ts 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](https://counterfact.dev/docs/patterns/test-context-not-handlers.html) --- ## 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. ```ts // api/scenarios/index.ts 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: ```ts // api/scenarios/index.ts 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); }; ``` ```ts // api/scenarios/pets.ts 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: ```ts // api/scenarios/index.ts export const soldOut: Scenario = ($) => { $.context.list().forEach((p) => { p.status = "sold"; }); }; ``` ``` ⬣> .scenario soldOut Applied soldOut ``` > **Docs:** > - [Pattern: Scenario Scripts](https://counterfact.dev/docs/patterns/scenario-scripts.html) > - [Feature: REPL](https://counterfact.dev/docs/features/repl.html) --- ## 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. ```ts // api/routes/_.context.ts export class Context { isRateLimited = false; isDown = false; // … rest of context … } ``` ```ts // api/routes/pet/{petId}.ts 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](https://counterfact.dev/docs/patterns/simulate-failures.html) --- ## Quick-reference: key documentation | Topic | URL | |---|---| | Getting started | https://counterfact.dev/docs/getting-started.html | | Full usage guide | https://counterfact.dev/docs/usage.html | | Reference (`$` param, CLI flags) | https://counterfact.dev/docs/reference.html | | State / context objects | https://counterfact.dev/docs/features/state.html | | REPL | https://counterfact.dev/docs/features/repl.html | | Pattern: Mock with Dummy Data | https://counterfact.dev/docs/patterns/mock-with-dummy-data.html | | Pattern: Federated Context Files | https://counterfact.dev/docs/patterns/federated-context.html | | Pattern: Test the Context, Not the Handlers | https://counterfact.dev/docs/patterns/test-context-not-handlers.html | | Pattern: Scenario Scripts | https://counterfact.dev/docs/patterns/scenario-scripts.html | | Pattern: Simulate Failures and Edge Cases | https://counterfact.dev/docs/patterns/simulate-failures.html | | Pattern: AI-Assisted Implementation | https://counterfact.dev/docs/patterns/ai-assisted-implementation.html | | Petstore example | https://github.com/counterfact/example-petstore | --- ## Checklist Work through these steps in order: - [ ] Run `npx counterfact@latest` to generate the initial scaffold - [ ] Read the real API's documentation and note key business rules and data shapes - [ ] Implement a `_.context.ts` with typed methods for all CRUD operations - [ ] Update each handler to delegate to the context instead of calling `.random()` - [ ] Write unit tests for the context class (not the handlers) - [ ] Export a `startup` function from `scenarios/index.ts` that seeds realistic data - [ ] Add named scenario exports for useful non-default states (empty, error, edge cases) - [ ] Verify the server starts with realistic data by running it and checking the REPL