Everything you want to know before you commit to using Counterfact.
- What is Counterfact?
- Who is it for?
- How do I start?
- What does it generate?
- Do I have to write TypeScript?
- Should I commit the generated code?
- Can it behave like a real backend?
- How does state work?
- What happens when I edit a file?
- Can I interact with the server without writing code?
- What if the real API exists but isn't ready for all paths?
- How does type safety work?
- Does it validate incoming requests?
- What OpenAPI versions are supported?
- Can I have more granular control over code generation?
- Will regenerating overwrite my changes?
- Can I use it programmatically (not via the CLI)?
- Can I add custom middleware?
- What's with these
nevertypes in the generated code? - Do I need to restart after changing a routes file?
- When do I need to restart?
- Where does the generated code go?
Counterfact is a tool that turns an OpenAPI spec into a live, editable, stateful mock server — in one command. It generates TypeScript route files (one per endpoint), starts an HTTP server, and lets you customize behavior by editing those files while the server keeps running.
Frontend developers who can't wait for the backend.
API-first teams who write the spec before writing any code and want to validate the design immediately.
QA engineers who need to reproduce specific edge cases and error states on demand.
Developers writing automated tests — Counterfact runs a real HTTP server, so it works seamlessly in integration and end-to-end test suites. Start it in a beforeAll, run your tests, tear it down in afterAll.
AI agents that make calls to third-party APIs — running against a local mock avoids rate limits, costs, and network flakiness.
Developers exploring a new API before they have production credentials or a signed contract.
One command:
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json apiReplace the Petstore URL with your own spec. A routes/ directory and types/ directory appear in api/, and a server starts at http://localhost:3100. No config file required.
Requires Node ≥ 22.0.0
For a complete worked example — the Swagger Petstore fully implemented with custom logic and state — see counterfact/example-petstore.
Two things:
-
routes/— one.tsfile per path in your OpenAPI spec. Each file exports a named function per HTTP method. By default they return random, schema-valid responses. -
types/— TypeScript interfaces for every request and response defined in the spec, including path params, query params, headers, and request/response bodies.
No. The generated files are valid TypeScript but you don't need to compile them yourself — Counterfact runs them directly. If you prefer plain JavaScript, you can rename the files and remove the type annotations. You'll lose autocomplete and compile-time safety, but the server will still work.
Yes, commit everything.
Commit the routes/ directory — that's your code. Counterfact generates starter files and after that doesn't touch them again.
In theory the types/ directory can be reproduced from the spec, so you might .gitignore it. But if the spec is owned by another team or organization, committing types/ means the team always has a working snapshot without needing to re-run generation. Counterfact creates a .gitignore that excludes .cache/ — do commit .gitignore, don't commit .cache/.
Yes — that's the point. A generated route is just a function. You can write real logic in it:
export const POST: HTTP_POST = ($) => {
if (!$.body.name) return $.response[400].text("name is required");
const pet = $.context.add($.body);
return $.response[200].json(pet);
};POST data and GET it back. Enforce validation. Simulate business rules. The server behaves like a real API because under the hood it is one, just running locally with TypeScript files you control.
Create a _.context.ts file next to your routes. All routes in the same directory (and subdirectories) share the same Context instance:
// routes/_.context.ts
export class Context {
private orders = new Map<number, Order>();
private id = 1;
place(order: Omit<Order, "id">): Order {
const record = { ...order, id: this.id++ };
this.orders.set(record.id, record);
return record;
}
get(id: number) { return this.orders.get(id); }
list() { return [...this.orders.values()]; }
}State is in-memory, which means it's lightning-fast, it resets to a known clean state every restart, and scalability isn't something you need to worry about because you're the only one using it.
The server hot-reloads it. The new handler takes effect immediately — no restart, no data loss. Your context (in-memory state) is preserved across reloads.
Yes. The REPL is a JavaScript prompt connected directly to the running server. Type expressions, call methods, inspect state, send requests:
⬣> context.list()
[ { id: 1, status: 'placed', petId: 42 } ]
⬣> context.place({ status: 'approved', petId: 99 })
{ id: 2, status: 'approved', petId: 99 }
⬣> client.get("/store/order/1")
{ status: 200, body: { id: 1, status: 'placed', petId: 42 } }
Use the --proxy-url flag to forward requests to the real backend and mock the paths that aren't ready yet:
npx counterfact@latest openapi.yaml api --proxy-url https://api.example.comYou can toggle individual paths from the REPL at runtime:
⬣> .proxy on /payments # forward /payments/* to the real API
⬣> .proxy off /payments # mock /payments/* again
⬣> .proxy off # mock everything
Every route handler type is generated from the spec. When you call .json(...) on a response builder, TypeScript checks that the object you pass matches the schema defined in the spec. When the spec changes and you regenerate, TypeScript surfaces every handler that no longer matches — before any request fails.
OpenAPI descriptions are preserved as JSDoc comments on the generated types, so they appear inline in your editor as you type.
Yes, by default. Requests that don't match the schema defined in the spec return a 400 automatically. Disable this with --no-validate-request if you need looser behavior.
Yes, by default. Response headers are validated against the schema defined in the spec. Any validation errors (missing required headers or type mismatches) are reported as response-type-error HTTP response headers — one header per error; multiple headers with the same name are allowed. The response body is still returned normally — the errors are advisory only. Disable this with --no-validate-response if you need looser behavior.
OpenAPI 3.x. Swagger 2 (OAS2) is not currently supported.
Yes. Use --generate-types or --generate-routes to generate only one or the other. Use --watch-types or --watch-routes to watch selectively. This is useful when you're mid-feature and don't want unrelated type updates cluttering your staging area.
npx counterfact@latest my-api.yaml api --generate-typesNo. Counterfact only writes files that don't already exist. Your custom route logic is safe when you regenerate after a spec update. New routes are scaffolded; existing ones are left alone.
Yes. Import counterfact and call it with options:
import { counterfact } from "counterfact";
await counterfact("openapi.yaml", "api", { port: 4000, serve: true });This makes Counterfact easy to embed in test setups — start a server in beforeAll, stop it in afterAll, and run real HTTP requests against it in your tests.
Yes. Drop a _.middleware.ts file anywhere in the routes tree. It receives a standard Koa middleware function and applies to all routes in that subtree:
// routes/_.middleware.ts
import type { Middleware } from "koa";
const middleware: Middleware = async (ctx, next) => {
if (!ctx.headers.authorization) {
ctx.status = 401;
return;
}
await next();
};
export default middleware;If you see query: never in a types file it means the OpenAPI spec doesn't define any query parameters for that operation. TypeScript will flag $.query.anything as a type error — by design. If the spec is incomplete, fix it at the source.
No. Counterfact watches the routes directory and hot-reloads changed files immediately. Your in-memory context is preserved across reloads.
If your OpenAPI spec is a remote URL, Counterfact only fetches it once at startup. To pick up spec changes you need to restart (or use a local copy and --watch). If your spec is a local file, Counterfact watches it and regenerates types automatically — no restart required.
Wherever you tell it. The second argument to the CLI is the output directory. Use any path you like — inside or outside your project.
Create an issue — questions are welcome.