Counterfact is three tools in one:
- a code generator that converts an OpenAPI document to TypeScript route files
- a mock server optimized for front-end development workflows
- a live REPL for inspecting and manipulating server state at runtime
- Quick Start
- Generated Code
- Routes
- State: Context Objects
- TypeScript Native Mode
- Hot Reload
- REPL
- Proxy
- Middleware
- Programmatic API
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json apiThis generates TypeScript route files under api/ for the Swagger Petstore and starts the server. Swap in your own OpenAPI document URL (or local path) and change api to whatever directory you prefer.
Requires Node ≥ 17.0.0
Full CLI reference
Usage: counterfact [options] [openapi.yaml] [destination]
Arguments:
openapi.yaml path or URL to OpenAPI document, or "_" to skip (default: "_")
destination path where code is generated (default: ".")
Options:
--port <number> server port (default: 3100)
-o, --open open a browser after starting
-g, --generate generate route and type files
--generate-types generate types only
--generate-routes generate routes only
-w, --watch generate + watch for spec changes
--watch-types watch types only
--watch-routes watch routes only
-s, --serve start the mock server
-r, --repl start the REPL
--spec <string> path or URL to OpenAPI document (alternative to positional argument)
--proxy-url <string> forward all unhandled requests to this URL
--prefix <string> base path prefix (e.g. /api/v1)
--always-fake-optionals include optional fields in random responses
-b, --build-cache pre-compile routes and types without starting the server
--config <path> path to a counterfact.yaml config file
-h, --help display help
Using a counterfact.yaml config file
Any CLI option can also be placed in a counterfact.yaml file in the current working directory. Command-line flags always take precedence over values in the file.
# counterfact.yaml
spec: ./openapi.yaml
port: 8080
serve: true
repl: true
watch: true
proxy-url: https://api.example.com
prefix: /api/v1Kebab-case keys (e.g. proxy-url, always-fake-optionals) are accepted and normalised automatically.
Use --config <path> to load a config file from a non-default location:
npx counterfact@latest --config ./config/counterfact.yamlIf --config points to a file that does not exist, Counterfact exits with an error. If no --config flag is given and there is no counterfact.yaml in the current directory, startup proceeds normally using CLI options and defaults.
Using with npm or yarn instead of npx
Pin a specific version by adding Counterfact as a dev dependency:
"scripts": {
"mock": "counterfact https://petstore3.swagger.io/api/v3/openapi.json api"
},
"devDependencies": {
"counterfact": "^2.0.0"
}Then run npm run mock or yarn mock. This ensures every developer on the team uses the same version.
Running in TypeScript native mode — no build step required
By default Counterfact pre-compiles your route files into a .cache/ directory using the TypeScript compiler. If you run Counterfact under a TypeScript-aware runtime, it detects this automatically and skips compilation entirely — loading the .ts source files directly.
With tsx (recommended for simplicity)
Install tsx as a dev dependency:
npm install --save-dev tsx
# or
yarn add --dev tsxThen invoke counterfact via tsx:
npx tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watchOr in package.json:
"scripts": {
"mock": "tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch"
}With plain Node.js (Node 22.6+ required)
Node's built-in --experimental-strip-types removes TypeScript type annotations without any extra dependency. A small loader included with Counterfact handles the .js → .ts import remapping that the TypeScript codebase relies on:
node \
--experimental-strip-types \
--import ./node_modules/counterfact/bin/register-ts-loader.mjs \
./node_modules/counterfact/bin/counterfact.js \
openapi.yaml api --serve --watchOr in package.json:
"scripts": {
"mock": "node --experimental-strip-types --import ./node_modules/counterfact/bin/register-ts-loader.mjs ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch"
}Note:
--experimental-strip-typesis stable enough for development use but the flag name may change before it graduates from experimental status.
Counterfact generates two directories from your OpenAPI document:
- 📂
types/— fully typed request/response interfaces, auto-regenerated whenever the OpenAPI document changes. Don't edit these by hand. - 📂
routes/— one TypeScript file per API path. These are yours to edit. Out of the box each file returns a random, schema-valid response. You can leave them as-is or customize as much as you like.
See Generated Code FAQ for questions about source control, editing, and regeneration.
No OpenAPI document? See using Counterfact without OpenAPI.
Each file in routes/ corresponds to an API path. For example, /users/{userId} maps to routes/users/{userId}.ts. The root path / maps to routes/index.ts.
A freshly generated route file looks like this:
export const GET: HTTP_GET = ($) => {
return $.response[200].random();
};
export const POST: HTTP_POST = ($) => {
return $.response[200].random();
};Each exported function handles one HTTP method. The single argument $ gives you everything you need: request data, response builders, server state, and utilities.
Tip
If you know Express, think of $ as a type-safe combination of req and res.
$.response is a fluent builder for HTTP responses. Start by picking a status code, then chain one or more methods:
| Method | Description |
|---|---|
.random() |
Returns random data generated from the OpenAPI schema (uses examples where available) |
.example(name) |
Returns a specific named example from the OpenAPI spec |
.json(content) |
Returns a JSON body (also converts to XML automatically when the client requests it) |
.text(content) |
Returns a plain-text body |
.html(content) |
Returns an HTML body |
.xml(content) |
Returns an XML body |
.match(contentType, content) |
Returns a body with an explicit content type; chain multiple for content negotiation |
.header(name, value) |
Adds a response header |
return $.response[200].header("x-request-id", "abc123").json({ ok: true });Use .cookie(name, value, options?) to set one or more cookies. Each call appends a new Set-Cookie header and returns the builder for chaining.
return $.response[200]
.cookie("sessionId", "abc123", {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 3600,
})
.json({ ok: true });Multiple cookies:
return $.response[200]
.cookie("sessionId", "abc123")
.cookie("theme", "dark")
.json({ ok: true });Supported options:
| Option | Description |
|---|---|
path |
Cookie path (e.g. "/") |
domain |
Cookie domain |
maxAge |
Max age in seconds (Max-Age) |
expires |
Expiry Date object (Expires) |
httpOnly |
Sets the HttpOnly flag when true |
secure |
Sets the Secure flag when true |
sameSite |
"lax", "strict", or "none" (SameSite) |
If your OpenAPI spec defines named examples for a response, you can return a specific one by name using .example(). The name is autocompleted and type-checked — passing an unknown name is a compile error.
// Return a specific named example
return $.response[200].example("successResponse");
// Chain additional decorations after selecting an example
return $.response[200]
.example("successResponse")
.header("x-request-id", "abc123");Tip
Your IDE's autocomplete knows which status codes, headers, and response shapes are valid for each endpoint — based directly on your OpenAPI spec. If you omit a required header, TypeScript will tell you. When the spec changes and types are regenerated, TypeScript will surface any mismatches.
The request is exposed through four typed properties:
| Property | Contents |
|---|---|
$.path |
Path parameters (e.g. $.path.userId for /users/{userId}) |
$.query |
Query string parameters |
$.headers |
Request headers |
$.body |
Request body |
export const GET: HTTP_GET = ($) => {
if ($.headers["x-token"] !== "super-secret") {
return $.response[401].text("Unauthorized");
}
const content =
`Results for "${$.query.keyword}" in ${$.path.groupName}` +
` with tags: ${$.body.tags.join(", ")}`;
return $.response[200].text(content);
};All four objects are typed from your OpenAPI spec, so autocomplete works for parameter names and values.
When a request includes HTTP Basic credentials, they're available at $.auth.username and $.auth.password.
Support for other security schemes (API key, OAuth 2, OpenID Connect, mutual TLS) is planned. Open an issue to help prioritize.
Counterfact responds much faster than a real server. To test loading states and timeouts, use $.delay():
// pause for exactly one second
await $.delay(1000);
// pause for a random duration between 1 and 5 seconds
await $.delay(1000, 5000);Counterfact translates your OpenAPI spec into strict TypeScript types. If your spec is incomplete, or you need to return something outside the spec (like a 500 error that isn't documented), the strict types can get in the way.
$.x is an alias for $ with all types widened to any, giving you an escape hatch:
export const GET: HTTP_GET = ($) => {
// header not defined in OpenAPI spec
$.headers["my-undocumented-header"]; // TypeScript error
$.x.headers["my-undocumented-header"]; // ok
// status code not defined in OpenAPI spec
return $.response[500].text("Error!"); // TypeScript error
return $.x.response[500].text("Error!"); // ok
};The $.context object is how routes share in-memory state. It's an instance of the Context class exported from _.context.ts in the same directory (or the nearest parent directory that has one).
// routes/pet.ts
export const POST: HTTP_POST = ($) => {
return $.response[200].json($.context.addPet($.body));
};
// routes/pet/{petId}.ts
export const GET: HTTP_GET = ($) => {
const pet = $.context.getPetById($.path.petId);
if (pet === undefined)
return $.response[404].text(`Pet ${$.path.petId} not found.`);
return $.response[200].json(pet);
};Customize _.context.ts to hold whatever state and business logic your mock needs:
// routes/_.context.ts
export class Context {
pets: Pet[] = [];
addPet(pet: Pet) {
const id = this.pets.length;
this.pets.push({ ...pet, id });
return this.pets[id];
}
getPetById(id: number) {
return this.pets[id];
}
}Important
Keep context in memory. Counterfact is a development tool — starting fresh each time is a feature, not a bug. In-memory state also makes the server very fast.
For large APIs you can nest context objects. Any subdirectory can have its own _.context.ts. One context can access another via the loadContext function passed to its constructor:
// routes/users/_.context.ts
export class Context {
constructor({ loadContext }) {
this.rootContext = loadContext("/");
this.petsContext = loadContext("/pets");
}
}Use the readJson function (also passed to the constructor) to load static JSON data into your context. The path is resolved relative to the _.context.ts file.
// routes/_.context.ts
export class Context {
private readonly readJson: (path: string) => Promise<unknown>;
constructor({ readJson }: { readJson: (path: string) => Promise<unknown> }) {
this.readJson = readJson;
}
async getSeeds() {
return this.readJson("../mocks/seeds.json");
}
}By default Counterfact compiles your route files into a .cache/ directory before loading them. When you run Counterfact under a TypeScript-aware runtime it detects this automatically and skips compilation, loading .ts source files directly. The result is the same hot-reload experience with no build step.
At startup Counterfact writes a small temporary TypeScript file to a system temp directory and attempts to import it. If the import succeeds the runtime is TypeScript-capable and the transpiler is skipped. No configuration is needed.
Invoke the counterfact binary through tsx:
# one-off via npx
npx tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch
# or in package.json scripts
"mock": "tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch"tsx is available as a dev dependency (npm install --save-dev tsx).
Node 22.6+ ships with --experimental-strip-types. A small module hook bundled with Counterfact (bin/register-ts-loader.mjs) adds the .js → .ts import remapping that Node doesn't do on its own:
node \
--experimental-strip-types \
--import ./node_modules/counterfact/bin/register-ts-loader.mjs \
./node_modules/counterfact/bin/counterfact.js \
openapi.yaml api --serve --watchIn package.json:
"scripts": {
"mock": "node --experimental-strip-types --import ./node_modules/counterfact/bin/register-ts-loader.mjs ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch"
}| Default (compiled) | Native TS | |
|---|---|---|
| Startup | Compiles routes to .cache/ first |
Loads .ts files directly |
.cache/ directory |
Created and managed automatically | Not used |
| Dependencies | None extra | tsx or Node 22.6+ |
| Route file format | Generated .ts files (same as always) |
Same |
| Hot reload | ✅ | ✅ |
Save a file — any route or context file — and the running server picks it up immediately. No restart needed, and in-memory state is preserved across reloads.
This makes it fast to set up edge cases like:
- What does the UI do 8 clicks deep when the server returns a 500?
- What if there are zero results? What if there are 10,000?
- What if the server is slow?
Find the file corresponding to the route, change behavior by editing the TypeScript code, and continue testing.
Depending on the scenario, you may want to commmit your changes to source control or throw them away.
The REPL is a JavaScript prompt connected directly to the running server — like the browser DevTools console, but for your mock API. After starting Counterfact you'll see:
____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___
|___ [__] |__| |\| | |=== |--< |--- |--| |___ |
Storybook for the back-end
| API Base URL ==> http://localhost:3100
| Admin Console ==> http://localhost:3100/counterfact/
⬣>
At the prompt you can interact with the live context:
// add a single pet
context.addPet({ name: "Fluffy", photoUrls: [] });
// add 100 pets
for (let i = 0; i < 100; i++)
context.addPet({ name: `Pet ${i}`, photoUrls: [] });
// query state
context.pets.filter((pet) => pet.name.startsWith("F"));To access context from a subdirectory:
const petsContext = loadContext("/pets");The built-in client object lets you make HTTP requests from the prompt without leaving the terminal:
client.get("/users");
client.post("/users", { name: "bob" });
client.put("/users/1", { name: "robert" }, { "x-api-version": "2" });All standard HTTP methods are supported. Arguments are: path, body (where applicable), headers.
The built-in route() function creates a fluent request builder that validates required parameters against your OpenAPI document before sending:
// Build and inspect before sending
const req = route("/pet/{petId}").method("get").path({ petId: 42 })
req.ready() // true / false
req.missing() // lists missing required parameters
req.help() // prints OpenAPI docs for the operation
await req.send()See the Route Builder guide for full documentation.
For more complex setups you can automate REPL interactions by writing scenario scripts — plain TypeScript files that export named functions. Run them with .apply:
⬣> .apply soldPets
Path resolution: the argument to .apply is a slash-separated path. The last segment is the function name; everything before it is the file path, resolved relative to <basePath>/scenarios/ (with index.ts as the default file).
| Command | File | Function |
|---|---|---|
.apply foo |
scenarios/index.ts |
foo |
.apply foo/bar |
scenarios/foo.ts |
bar |
.apply foo/bar/baz |
scenarios/foo/bar.ts |
baz |
A scenario function receives a single argument with { context, loadContext, routes, route }:
// scenarios/index.ts
import type { Scenario } from "../types/scenario-context.js";
export const soldPets: Scenario = ($) => {
// Mutate context directly — same as typing in the REPL
$.context.petService.reset();
$.context.petService.addPet({ id: 1, status: "sold" });
$.context.petService.addPet({ id: 2, status: "available" });
// Store a pre-configured route builder for later use in the REPL
$.routes.findSold = $
.route("/pet/findByStatus")
.method("get")
.query({ status: "sold" });
}After the command runs you can immediately use anything stored in $.routes:
⬣> routes.findSold.send()The Scenario type and ApplyContext interface are generated automatically into types/scenario-context.ts when you run Counterfact with type generation enabled.
You can mix real backend calls with mocks — useful when some endpoints are not finished or you need to test edge cases like 500 errors.
To proxy a single endpoint from within a route file:
// routes/pet/{petId}.ts
export const GET: HTTP_GET = ($) => {
return $.proxy("https://uat.petstore.example.com/pet");
};To set a proxy for then entire API at runtime pass --proxy-url on the CLI:
npx counterfact@latest openapi.yaml api --proxy-url https://uat.petstore.example.comFrom the REPL, you can toggle proxying for the whole API or specific routes:
⬣> .proxy on /payments # forward /payments to the real API
⬣> .proxy off /payments # let Counterfact handle /payments
⬣> .proxy off # stop proxying everything
Type .proxy help in the REPL for the full list of proxy commands.
Place a _.middleware.ts file in any routes/ subdirectory to intercept requests and responses for that subtree. Middleware applies from the root down — a _.middleware.ts at the root runs for every request.
// routes/_.middleware.ts
export async function middleware($, respondTo) {
const response = await respondTo($);
return response.header("X-Custom-Header", "Custom Value");
}respondTo($) passes the request to the next middleware layer or the route handler, and returns the response. You can modify $ before calling respondTo, modify the response after, or both.
Counterfact can be used as a library — for example, from Playwright or Cypress tests. This lets you manipulate context state directly in test code without relying on special magic values in mock logic.
import { counterfact } from "counterfact";
const config = {
basePath: "./api", // directory containing your routes/
openApiPath: "./api.yaml", // optional; pass "_" to run without a spec
port: 8100,
alwaysFakeOptionals: false,
generate: { routes: false, types: false },
proxyPaths: new Map(),
proxyUrl: "",
routePrefix: "",
startAdminApi: false,
startRepl: false, // do not auto-start the REPL
startServer: true,
watch: { routes: false, types: false },
};
const { contextRegistry, start } = await counterfact(config);
const { stop } = await start(config);
// Get the root context — the object your routes see as $.context
const rootContext = contextRegistry.find("/");Once you have rootContext you can read and write any state that your route handlers expose.
Given this route handler:
// routes/auth/login.ts
export const POST: HTTP_POST = ($) => {
if ($.context.passwordResponse === "ok") return $.response[200];
if ($.context.passwordResponse === "expired")
return $.response[403].header("reason", "expired-password");
return $.response[401];
};A Playwright test can flip between scenarios without hard-coded usernames:
import { counterfact } from "counterfact";
import { chromium } from "playwright";
let page;
let rootContext;
let page;
let rootContext;
let stop;
let browser;
beforeAll(async () => {
browser = await chromium.launch({ headless: true });
page = await (await browser.newContext()).newPage();
const { contextRegistry, start } = await counterfact(config);
({ stop } = await start(config));
rootContext = contextRegistry.find("/");
});
afterAll(async () => {
await stop();
await browser.close();
});
it("rejects an incorrect password", async () => {
rootContext.passwordResponse = "incorrect";
await attemptToLogIn();
expect(await page.isVisible("#authentication-error")).toBe(true);
});
it("loads the dashboard on success", async () => {
rootContext.passwordResponse = "ok";
await attemptToLogIn();
expect(await page.isVisible("#dashboard")).toBe(true);
});
it("prompts for a password change when the password has expired", async () => {
rootContext.passwordResponse = "expired";
await attemptToLogIn();
expect(await page.isVisible("#password-change-form")).toBe(true);
});| Property | Type | Description |
|---|---|---|
contextRegistry |
ContextRegistry |
Registry of all context objects keyed by path. Call .find(path) to get the context for a given route prefix. |
registry |
Registry |
Registry of all loaded route modules. |
koaApp |
Koa |
The underlying Koa application. |
koaMiddleware |
Koa.Middleware |
The Counterfact request-dispatch middleware. |
start(config) |
async (config) => { stop() } |
Starts the server (and optionally the file watcher and code generator). Returns a stop() function to gracefully shut down. |
startRepl() |
() => REPLServer |
Starts the interactive REPL. Returns the REPL server instance. |
- Getting started
- Usage patterns — explore an API, simulate failures, hybrid proxy, agentic coding, and more
- Reference —
$parameter, response builder methods, full CLI flags, architecture overview - FAQ — common questions about state, type safety, regeneration, and programmatic use
- How it compares — side-by-side with json-server, WireMock, Prism, Microcks, and MSW
- Generated code FAQ — questions about source control, editing, and regeneration
- Petstore example — a complete worked example