Generated Code
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 the FAQ for common questions about source control, editing, and regeneration.
No OpenAPI document? See using Counterfact without OpenAPI.
Generated file structure
The output directory is called basePath in library options and destination in the CLI YAML config. Counterfact creates routes/_.context.ts with a [key: string]: any index signature so you can add shared state immediately. Remove that signature and declare fields to enable strict typing. Existing context files are preserved. The context and optional store files below are yours to customize: a context shares behavior within a route subtree; a store shares state across separately configured API groups.
<output-directory>/
├── _.store.ts # application-level state shared across API groups (optional)
├── routes/
│ ├── _.context.ts # shared in-memory state (optional)
│ ├── _.middleware.ts # Counterfact middleware (optional)
│ ├── pet.ts # handlers for /pet
│ ├── pet/
│ │ └── {petId}.ts # handlers for /pet/{petId}
│ └── store/
│ └── order.ts
└── types/
├── index.ts # public generated 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.
Generated types preserve literal names and enum values, including escaped characters. Nested allOf, anyOf, and oneOf schemas retain their grouping. Unsupported schema type strings produce unknown; use standard OpenAPI types and $ref references for named schemas.
Schema types also preserve:
- OpenAPI 3.0
nullable: trueon declared types and OpenAPI 3.1 type unions containingnull. A required nullable property must still be present. Explicitenumandconstconstraints continue to restrict the allowed values. - Primitive
constvalues, such asconst: cat, as literal types. - Object properties when
type: objectis omitted, and typed dictionary values declared withadditionalProperties, including$refvalues. Unrestricted dictionary values remainunknown. - Properties alongside composition keywords and required fields declared by a containing schema. Requiring a referenced model’s field in one composed type does not change the shared model.
- Explicit discriminator mappings on referenced
oneOfandanyOfbranches, so checking the discriminator narrows the generated union. Multiple mapping values may select the same branch. OpenAPI 3.2defaultMappingremains a fallback branch. - Tuple positions declared with OpenAPI 3.1
prefixItems(or an array-valueditems).minItemsdetermines which prefix positions are required,items: falsecloses the tuple, and a schema-valueditemstypes its remaining elements. Length constraints beyond the declared prefix are not encoded in the TypeScript type.
Generation does not replace runtime validation. TypeScript cannot express every JSON Schema constraint. In particular, oneOf is represented as a union rather than a guarantee that exactly one branch validates.
Generated operation argument types include only the request inputs available for that operation. For example, an operation without query parameters has no $.query property. Versioned arguments include their version literal; unversioned arguments omit version. These properties are selected during generation, so the emitted argument is a plain object type.
Update a specification
With specification watching enabled, save the OpenAPI document to regenerate the enabled artifacts. Types reflect the updated contract; existing route implementations retain your edits. Use your editor or TypeScript checks to find handlers whose assumptions no longer match the specification.
New operations receive route scaffolding. If you want to remove obsolete route files, use --prune deliberately and review the resulting diff. Commit your specification and custom behavior so changes to the simulation can be reviewed together.
The watch generation options control responses to specification changes. Runtime hot reload of route files is separate. See CLI configuration and hot reload.
For a concrete change, expected diagnostic, and CI command, follow Catch a contract change with TypeScript. Serving TypeScript does not substitute for that check.
Explore a contract while designing it
Run npx counterfact@latest openapi.yaml api --watch --serve while editing a local contract. New required fields appear in generated sample responses; optional fields may be omitted unless --always-fake-optionals is enabled. Update fixed handlers and run an explicit TypeScript check after regeneration. The server continuing to run does not establish that custom handlers still match the contract.
Central type exports
Import generated API types from types/index.ts:
// routes/pet/{petId}.ts
import type { Components, Routes, Schemas } from "../../types/index.js";
type Route = Routes["/pet"]["/{petId}"];
type Pet = Schemas["Pet"];
type SamePet = Components["schemas"]["Pet"];
export const GET: Route["GET"] = async ($) => {
return $.response.status(200).random();
};
New route files declare a local Route alias for their path, even when they
contain only one operation. Each handler selects its method with Route["GET"],
Route["POST"], and so on. The alias is local to the file and is not exported.
Existing handler text is preserved during regeneration; newly added methods reuse
a matching local alias, or receive a new alias with a numeric suffix if Route
is already taken.
Routes is a tree of path segments. Each segment keeps its leading /, and
uppercase HTTP methods select the handler contract at that branch. A branch can
contain both operations on its own path and child branches: /hello and
/hello/world share the Routes["/hello"] branch.
For GET operations, the indexes are:
| OpenAPI path | Handler type |
|---|---|
/hello | Routes["/hello"]["GET"] |
/hello/world | Routes["/hello"]["/world"]["GET"] |
/pets/{petId} | Routes["/pets"]["/{petId}"]["GET"] |
/ | Routes["/"]["GET"] |
/hello/world/ | Routes["/hello"]["/world"]["/"]["GET"] |
Each handler retains its typed inputs, context, and response builders. Parameter
segments use their literal names, such as /{petId}. A terminal slash is an
empty / segment, preserving the spec’s original spelling; generated route
filenames still use Counterfact’s normalized paths.
For an existing full-path annotation, split the path into segment indexes. Regeneration preserves existing handler text, so update those annotations in your code when adopting the tree.
Components maps the original OpenAPI component categories and names. Schemas
is an alias of Components["schemas"]. All declared components are included,
even when no operation references them. Absent categories are empty maps.
| Category | Generated representation |
|---|---|
schemas | The schema’s data type |
parameters, headers | The value’s schema type, or its content map when it uses content |
responses | Counterfact’s response contract: headers, requiredHeaders, content, and examples; media entries expose their data under schema |
requestBodies, mediaTypes | The definition’s structure, with embedded schema and itemSchema fields replaced by data types |
examples, securitySchemes, links | The definition’s literal structure |
callbacks, pathItems | The definition’s structure, with embedded schemas replaced by data types |
For example, access a JSON response body through
Components["responses"]["NotFound"]["content"]["application/json"]["schema"].
Component maps describe types; they do not export runtime values or configure
server authentication, callbacks, or links.
Direct imports from generated operation and component modules are deprecated.
Import Routes, Components, and Schemas from types/index.js in new and
updated code. Existing deep imports continue to work for compatibility;
regeneration does not rewrite existing handler annotations to the new style.
Context$, Scenario, and Scenario$ are direct exports of the same central
entry point. Middleware is a directory-indexed map: use Middleware["/"]
or Middleware["/admin"] for the matching route directory. Dedicated helper
imports are also deprecated and remain available for compatibility.
New route handlers use Routes; regeneration preserves existing handlers and
imports. Each configured API group has its own central entry point. For versions
in the same group, Routes uses the shared version-aware handler contracts and a
component with the same name in multiple versions is the union of those types.
Version-specific files remain compatibility surfaces; the central component maps
expose unions across versions.
Context, scenario, and middleware types
Handler context types follow the same lookup as runtime: an endpoint such as
/dictionaries uses routes/dictionaries/_.context.ts when it exists.
Descendant endpoints inherit that context unless a closer context is defined.
Otherwise, lookup falls back through ancestor directories to routes/_.context.ts.
This applies to both ordinary and versioned APIs.
Use the same central import for constructor helpers and scenarios:
// routes/_.context.ts
import type { Context$ } from "../types/index.js";
export class Context {
constructor($: Context$) {
void $;
}
}
// scenarios/index.ts
import type { Scenario, Scenario$ } from "../types/index.js";
function initialize($: Scenario$): void {
void $;
}
export const startup: Scenario = function ($) {
initialize($);
};
These types are shared within an API group. Context$ includes typed
loadContext() overloads, readJson(), and the shared store when configured.
Scenario$ includes the root context, context lookups, and route-builder helpers.
It does not expose the shared store directly.
Middleware types vary by directory because each directory can have a different
nearest context. Select its directory path from Middleware:
// routes/admin/_.middleware.ts
import type { Middleware } from "../../types/index.js";
export const middleware: Middleware["/admin"] = async function ($, respondTo) {
return respondTo($);
};
Use Middleware["/"] at the root. The map contains entries for directories in
the route tree and follows inherited contexts. Directory changes refresh the
map; editing a context updates the referenced type. See middleware.
Multi-version APIs
First configure versions in the same API group. Select the shared handler contract through Routes; it describes the possible request/response contracts and lets $.minVersion() select the applicable versions. The following handler assumes GET /pets declares a 200 response in v1 and v2.
When two or more versioned specs share an operation, its Routes entry describes
the supported versions. The import and annotation stay the same as for an
unversioned API:
// routes/pets.ts
import type { Routes } from "../types/index.js";
type Route = Routes["/pets"];
export const GET: Route["GET"] = function ($) {
if ($.minVersion("v2")) {
return $.response.status(200).random();
}
return $.response.status(200).random();
};
Counterfact combines each version’s request arguments behind this entry point.
Use $.minVersion("v2") to narrow $ to versions at or after v2 in the
configured order and access their version-specific fields. You do not need to
import the internal versioned operation modules.
See also
- Routes — writing route handlers, reading request data, building responses
- State — sharing state across routes with context objects
- Find your workflow
The root endpoint always uses the root context, even though its generated type file is named index.types.ts. A real /index child context applies only to that child endpoint and its descendants.
Optional request bodies include undefined in the generated $.body type for
JSON, URL-encoded forms, and multipart forms. In OpenAPI 3, a body is optional
when requestBody.required is false or omitted. Guard or normalize $.body
before accessing its properties, for example const body = $.body ?? {};.
Required bodies exclude undefined; nullable schemas independently allow null.