Reference

Complete reference for Counterfact’s architecture, route handlers, and CLI.


Contents


Architecture overview

OpenAPI spec (YAML or JSON, local or URL)


┌──────────────────────┐
│ TypeScript Generator │  → routes/  (one .ts per path)
│                      │  → types/   (request/response interfaces)
└──────────────────────┘


┌──────────────────────┐
│  Koa HTTP Server     │  → dispatches requests to route handlers
│  + Hot Reload        │  → watches for file changes via chokidar
│  + REPL              │  → interactive terminal attached to live state
│  + Proxy             │  → optional passthrough to a real backend
└──────────────────────┘

Generated file structure

<output-directory>/
├── routes/
│   ├── _.context.ts           # shared in-memory state (optional)
│   ├── _.middleware.ts        # custom Koa middleware (optional)
│   ├── pet.ts                 # handlers for /pet
│   ├── pet/
│   │   └── {petId}.ts         # handlers for /pet/{petId}
│   └── store/
│       └── order.ts
└── types/
    └── paths/
        ├── pet.types.ts
        ├── pet/
        │   └── {petId}.types.ts
        └── store/
            └── order.types.ts

Note: Files under types/ are automatically regenerated whenever the OpenAPI spec changes. Never edit them by hand — your changes will be overwritten on the next regeneration.


Route handlers

Every generated route file exports a named function per HTTP method. The function receives a single $ parameter that exposes everything from the request and a response builder typed to the spec.

Default: random schema-valid response

// routes/pet/{petId}.ts
import type { HTTP_GET } from "../../types/paths/pet/{petId}.types.js";

export const GET: HTTP_GET = ($) => {
  return $.response[200].random();
};

Custom response

export const GET: HTTP_GET = ($) => {
  const pet = db.find($.path.petId);
  if (!pet) return $.response[404].text(`Pet ${$.path.petId} not found`);
  return $.response[200].json(pet);
};

Counterfact handles content negotiation automatically. Calling .json(content) will also serve the same data as XML when the client sends Accept: application/xml.

Named OpenAPI example

export const GET: HTTP_GET = ($) => {
  return $.response[200].example("fullPet");
  //                              ^ autocompleted from your spec
};

The $ parameter

PropertyTypeDescription
$.pathtyped objectPath parameters from the URL
$.querytyped objectQuery string parameters
$.headerstyped objectRequest headers
$.bodytyped objectParsed request body
$.contextContext instanceShared state for this route subtree
$.response[N]response builderFluent builder for HTTP status code N (e.g. $.response[200], $.response[404])

Response builder methods

$.response[N] (where N is the HTTP status code) returns a fluent builder. Chain one or more of these methods:

MethodDescription
.random()Random data generated from the OpenAPI schema (uses examples where available)
.example(name)A specific named example from the OpenAPI spec
.empty()Explicitly returns a response with no body (use for 204 No Content and similar)
.json(content)JSON body (also converts to XML automatically when the client requests it)
.text(content)Plain-text body
.html(content)HTML body
.xml(content)XML body
.match(contentType, content)Body with an explicit content type; chain multiple for content negotiation
.header(name, value)Adds a response header
.cookie(name, value, options?)Adds a Set-Cookie header
return $.response[200]
  .header("x-request-id", "abc123")
  .cookie("session", "xyz", { httpOnly: true })
  .json({ ok: true });

State management

Create a _.context.ts file anywhere in the routes tree. All route files in the same directory (and below) share the same Context instance.

// routes/_.context.ts
import type { Pet } from "../types/components/pet.types.js";

export class Context {
  private pets = new Map<number, Pet>();
  private nextId = 1;

  add(pet: Omit<Pet, "id">): Pet {
    const id = this.nextId++;
    const created = { ...pet, id };
    this.pets.set(id, created);
    return created;
  }

  get(id: number): Pet | undefined {
    return this.pets.get(id);
  }

  list(): Pet[] {
    return [...this.pets.values()];
  }

  remove(id: number): boolean {
    return this.pets.delete(id);
  }
}

Cross-context communication with loadContext()

Route handlers can reach into a different subtree’s context using the loadContext(path) function injected into every handler. This lets sibling or parent routes share data without merging everything into one big context.

// routes/payments/{id}.ts
export const GET: HTTP_GET = ($) => {
  // Load the context that owns /users, even though this route lives under /payments
  const usersContext = $.loadContext("/users") as import("../users/_.context.js").Context;
  const user = usersContext.getById($.query.userId);
  if (!user) return $.response[404].text("User not found");
  return $.response[200].json({ paymentId: $.path.id, user });
};

Hot reload

Counterfact watches the routes directory with chokidar. When you save a route file:

  1. The module is re-imported.
  2. The handler is swapped in the registry.
  3. The Context instance is preserved — in-memory data survives the reload.

No restart required.


Live REPL

The REPL runs in the terminal alongside the server. It connects directly to the live Context and route registry.

⬣> context.list()
[ { id: 1, name: 'Fluffy', status: 'available' } ]

⬣> context.add({ name: 'Rex', photoUrls: [], status: 'pending' })
{ id: 2, name: 'Rex', photoUrls: [], status: 'pending' }

⬣> client.get("/pet/1")
{ status: 200, body: { id: 1, name: 'Fluffy', status: 'available' } }

⬣> .proxy on /payments    # forward /payments/* to the real API
⬣> .proxy off             # disable all proxying

Hybrid proxy

Forward specific paths to a real backend while mocking the rest. Useful when only part of an API exists yet, or when you want to replace a few endpoints with custom behavior.

npx counterfact@latest openapi.yaml api --proxy-url https://api.example.com

Toggle individual paths at runtime from the REPL (see above).


Middleware

Drop a _.middleware.ts file into any routes subdirectory to inject Koa middleware for all routes in that subtree.

// routes/_.middleware.ts
import type { Middleware } from "koa";

const middleware: Middleware = async (ctx, next) => {
  ctx.set("x-powered-by", "counterfact");
  await next();
};

export default middleware;

Type safety

Route handler types are generated directly from the OpenAPI spec. When you regenerate after a spec change, TypeScript surfaces every handler that no longer matches the contract — at compile time, before anything breaks in production.

// This will fail to compile if status 200 no longer exists
// or if the response body shape changes.
export const GET: HTTP_GET = ($) => {
  return $.response[200].json({ id: $.path.petId, name: "Fluffy" });
};

OpenAPI descriptions are preserved as JSDoc comments on generated types, so they appear inline in your editor as you type.


Programmatic API

Import counterfact and call it directly instead of using the CLI:

import { counterfact } from "counterfact";

await counterfact("openapi.yaml", "api", { port: 4000, serve: true });

Multiple API versions

SpecConfig.version

The optional version field on a spec entry declares the version label for that spec (e.g. "v1", "v2").

When combined with group and no explicit prefix, the server mounts the spec’s routes under /<group>/<version>. When omitted, routes are mounted under /<group>.

When at least one spec in a group declares a non-empty version, Counterfact generates types/versions.ts inside that group’s subdirectory with the Versions, VersionsGTE, and Versioned types.

Version order is determined by the order of entries in the config — the first entry with a given group is the oldest version.

Versioned<T, V>

The Versioned type is the type of the $ argument in a versioned route handler. It is generated into <basePath>/<group>/types/versions.ts and is already used by the generated HTTP_GET (and other) handler types — you do not need to import it directly.

export type Versioned<
  T extends Partial<Record<Versions, object>>,
  V extends keyof T & Versions = keyof T & Versions,
> = T[V] & {
  version: V;
  minVersion<M extends keyof T & Versions>(
    min: M,
  ): this is Versioned<T, Extract<V, VersionsGTE[M]>>;
};
MemberDescription
TMap from version string to the $-arg type for that version
VUnion of currently active version keys (defaults to all keys of T)
versionThe version string for the current request (e.g. "v2")
minVersion(min)Type predicate; returns true when the current version is ≥ min in the declared order and narrows $ accordingly

Versions

A union of all version strings declared for a group (e.g. "v1" | "v2" | "v3"). Generated into types/versions.ts.

VersionsGTE

A mapped type that resolves, for each version, the set of versions that are greater than or equal to it. Used internally by Versioned.minVersion() to compute the narrowed type after a successful check.

types/versions.ts

This file is auto-generated once per API group whenever at least one spec in that group declares a non-empty version. It lives at <basePath>/<group>/types/versions.ts.

It exports:

ExportDescription
VersionsUnion of all version strings for the group
VersionsGTEMap from each version to the set of versions ≥ it
Versioned<T, V>The $-arg type for versioned handlers

Do not edit this file — it is regenerated automatically.

See the Multiple versions feature page for a full walkthrough.


CLI reference

npx counterfact@latest [spec] [output] [options]
FlagDefaultDescription
-p, --port <number>3100HTTP server port
-o, --openfalseOpen browser on start
-g, --generatefalseGenerate all code (routes and types)
-w, --watchfalseGenerate and watch all code for changes
-s, --servefalseStart the server
-r, --replfalseStart the REPL
-b, --build-cachefalseBuild the cache of compiled routes and types
--spec <path>(positional arg)Path or URL to the OpenAPI document
--proxy-url <url>(none)Default upstream for the proxy
--prefix <path>(none)Global path prefix (e.g. /api/v1)
--no-validate-requestDisable OpenAPI request validation
--no-validate-responseDisable OpenAPI response validation
--generate-typesfalseGenerate types only
--generate-routesfalseGenerate routes only
--watch-typesfalseWatch and regenerate types only
--watch-routesfalseWatch and regenerate routes only
--always-fake-optionalsfalseInclude optional fields in random responses
--prunefalseRemove route files that no longer exist in the spec
--admin-apifalseEnable the Admin API at /_counterfact/api/*
--admin-api-token <token>(none)Bearer token required for Admin API endpoints
--no-update-checkDisable the npm update check on startup
--config <path>counterfact.yamlPath to a config file

Run npx counterfact@latest --help for the full list.


See also