Catch a contract change with TypeScript
Use generated handler types to surface changed assumptions before clients and services meet in a shared environment. Counterfact regenerates contracts; your editor or an explicit tsc invocation performs the type check. Serving and hot reload transpile routes without running a full project typecheck.
Generate a small typed operation
In a project with "type": "module", install exact versions and commit the lockfile:
npm install --save-dev --save-exact counterfact [email protected]
Save status.yaml:
openapi: 3.0.3
info: { title: Status API, version: 1.0.0 }
paths:
/status:
get:
operationId: getStatus
responses:
"200":
description: Service status
content:
application/json:
schema:
type: object
required: [status]
properties:
status: { type: string, enum: [ready] }
Generate, then replace the editable route:
npx counterfact status.yaml api --generate --no-update-check
// api/routes/status.ts
import type { getStatus } from "../types/paths/status.types.js";
export const GET: getStatus = function ($) {
return $.response[200].json({ status: "ready" });
};
Save tsconfig.counterfact.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["api/**/*.ts"]
}
Run npx tsc --project tsconfig.counterfact.json. It should exit successfully.
Change the response contract
In the response schema, replace required: [status] with required: [status, message] and add message: { type: string } under properties. Regenerate and check:
npx counterfact status.yaml api --generate --no-update-check
npx tsc --project tsconfig.counterfact.json
The edited handler stays intact while its generated type changes. TypeScript should now exit nonzero with a diagnostic identifying the missing message property in the JSON response. Exact diagnostic wording depends on the compiler version.
Update the handler:
export const GET: getStatus = function ($) {
return $.response[200].json({ status: "ready", message: "Accepting requests" });
};
Run the same typecheck again; it should pass. In CI, run generation and this explicit check with pinned dependencies before the client tests. Commit the specification and authored changes together; generated types are derived artifacts, so follow your project’s source-control convention for them.
Understand the boundary
This demonstrates a structural authoring check for typed, included files. It does not prove every use of the API is correct: unchecked JavaScript, any, $.x, incomplete schemas, and unmodeled behavior can escape it. Runtime validation checks a different, limited set of traffic properties. Keep HTTP assertions and targeted real-backend tests alongside the typecheck; Counterfact provides earlier local feedback rather than replacing a gateway.
When checking responses, make advisory header errors fail your HTTP test:
const response = await fetch(`${baseUrl}/status`);
const body = await response.text();
assert.equal(response.status, 200, body);
assert.equal(response.headers.get("response-type-error"), null);
Here assert is node:assert/strict and baseUrl is the running simulator’s address. This checks the response status and supported header diagnostics; it is not a general response-body schema check.