Programmatic API

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: "",
  prefix: "",
  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.

Example: parameterised auth scenario with Playwright

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 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);
});

Multiple specs / versioned APIs

Pass a specs array as the second argument to counterfact() to host several API specs on the same server. Each entry is a SpecConfig object:

FieldTypeDescription
sourcestringPath or URL to the OpenAPI document ("_" to run without a spec).
groupstringSubdirectory under config.basePath for this spec’s generated route files.
versionstring (opt.)Version label (e.g. "v1"). Combined with group to derive the URL prefix.
prefixstring (opt.)Explicit URL prefix. Overrides the derived prefix when provided.

Automatic prefix derivation

When prefix is omitted, the server derives the URL prefix from group and version:

groupversionDerived prefix
setset/<group>/<version>
setabsent/<group>
absentabsent"" (root)

Example — serving two versions of the same API

import { counterfact } from "counterfact";

const { start } = await counterfact(config, [
  { source: "./api-v1.yaml", group: "my-api", version: "v1" },
  { source: "./api-v2.yaml", group: "my-api", version: "v2" },
]);

await start(config);
// Routes are now available at:
//   http://localhost:8100/my-api/v1/...
//   http://localhost:8100/my-api/v2/...

Pass an explicit prefix to override derivation:

const { start } = await counterfact(config, [
  { source: "./api.yaml", group: "my-api", version: "v1", prefix: "/legacy" },
]);
// Routes are served at /legacy/... regardless of group/version.

Return value of counterfact()

PropertyTypeDescription
contextRegistryContextRegistryRegistry of all context objects keyed by path. Call .find(path) to get the context for a given route prefix.
registryRegistryRegistry of all loaded route modules.
koaAppKoaThe underlying Koa application.
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()() => REPLServerStarts the interactive REPL. Returns the REPL server instance.

See also