User manual

Routes

Use the project installation setup to run these examples with Counterfact 3 or newer.

Each file in routes/ corresponds to an API path. For example, /users/{userId} maps to routes/users/{userId}.ts. The root path / maps to routes/index.ts.

Use typed function expressions for route handlers. This example assumes your spec declares GET and POST on /pets with 200 responses; retain the imports generated for your own path:

// routes/pets.ts — keep the imports generated for your operation
import type { Routes } from "../types/index.js";

type Route = Routes["/pets"];

export const GET: Route["GET"] = function ($) {
  return $.response.status(200).random();
};

export const POST: Route["POST"] = function ($) {
  return $.response.status(200).random();
};

Prefer function expressions with a block body and an explicit return in route files. You can add logging, local variables, or other statements without restructuring the handler. Convert generated arrow-function handlers to this form when editing them.

The generated handler type on the exported constant (for example, GET: Route["GET"]) gives $ its request type automatically. Leave $ unannotated, and use async function ($) when you need await.

Each exported function handles one HTTP method. The single argument $ gives you everything you need: request data, response builders, server state, and utilities.

Tip

If you know Express, think of $ as a type-safe combination of req and res.

Building responses with $.response

$.response is a fluent builder for HTTP responses. Start by picking a status code, then chain one or more methods:

MethodDescription
.random()Returns random data generated from the OpenAPI schema (uses examples where available)
.example(name)Returns a specific named example from the OpenAPI spec
.json(content)Returns a JSON body (also converts to XML automatically when the client requests it)
.text(content)Returns a plain-text body
.html(content)Returns an HTML body
.xml(content)Returns an XML body
.match(contentType, content)Returns a body with an explicit content type; chain multiple for content negotiation
.header(name, value)Adds a response header
return $.response
  .status(200)
  .header("x-request-id", "abc123")
  .json({ ok: true });

Setting cookies

Use .cookie(name, value, options?) to set one or more cookies. Each call appends a new Set-Cookie header and returns the builder for chaining.

return $.response
  .status(200)
  .cookie("sessionId", "abc123", {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: 3600,
  })
  .json({ ok: true });

Multiple cookies:

return $.response
  .status(200)
  .cookie("sessionId", "abc123")
  .cookie("theme", "dark")
  .json({ ok: true });

Supported options:

OptionDescription
pathCookie path (e.g. "/")
domainCookie domain
maxAgeMax age in seconds (Max-Age)
expiresExpiry Date object (Expires)
httpOnlySets the HttpOnly flag when true
secureSets the Secure flag when true
sameSite"lax", "strict", or "none" (SameSite)

Using named examples

If your OpenAPI spec defines named examples for a response, you can return a specific one by name using .example(). The name is autocompleted and type-checked — passing an unknown name is a compile error.

// Return a specific named example
return $.response.status(200).example("successResponse");

// Chain additional decorations after selecting an example
return $.response
  .status(200)
  .example("successResponse")
  .header("x-request-id", "abc123");

Tip

Your IDE’s autocomplete knows which status codes, headers, and response shapes are valid for each endpoint — based directly on your OpenAPI spec. If you omit a required header, TypeScript will tell you. When the spec changes and types are regenerated, TypeScript will surface any mismatches.

Reading request data

The request is exposed through four typed properties:

PropertyContents
$.pathPath parameters (e.g. $.path.userId for /users/{userId})
$.queryQuery string parameters
$.headersRequest headers
$.bodyParsed request body; undefined for GET and HEAD

This example assumes a generated POST /groups/{groupName}/search operation with a required string path parameter groupName, required string query parameter keyword, a string x-token header, and a required JSON body containing a required tags array of strings. Its 200 and 401 responses both declare text/plain.

// api/routes/groups/{groupName}/search.ts
import type { Routes } from "../../../types/index.js";

type Route = Routes["/groups"]["/{groupName}"]["/search"];

export const POST: Route["POST"] = function ($) {
  if ($.headers["x-token"] !== "super-secret") {
    return $.response.status(401).text("Unauthorized");
  }

  const content =
    `Results for "${$.query.keyword}" in ${$.path.groupName}` +
    ` with tags: ${$.body.tags.join(", ")}`;

  return $.response.status(200).text(content);
};

All four properties are typed from your OpenAPI spec, so autocomplete works for parameter names and values. GET and HEAD handlers receive no body; put their inputs in path, query, or header parameters.

Cookie and query parameter names such as constructor and __proto__ are request data. They are present only when supplied by the client. Duplicate cookies keep their first value.

Object query parameters (exploded form style)

When a query parameter has type: object in the spec, OpenAPI’s default serialisation is exploded form style: each property of the object becomes its own query parameter.

For example, a pageable parameter of type Pageable (with page, size, and sort properties) is sent like this:

GET /items?page=0&size=100&sort=name

Counterfact automatically assembles these individual query parameters back into the object, so you can access them as:

import type { Routes } from "../types/index.js";

type Route = Routes["/items"];

export const GET: Route["GET"] = function ($) {
  const { page, size, sort } = $.query.pageable;
  // page = "0", size = "100", sort = "name"
  // ...
};

This behaviour follows the OpenAPI 3.x defaults (style: form, explode: true) and also applies when those values are set explicitly. To opt out, set explode: false on the parameter in your spec.

Basic auth: $.auth

When a request includes HTTP Basic credentials, they’re available at $.auth.username and $.auth.password.

When your OpenAPI document defines an apiKey security scheme (in: header, in: query, or in: cookie), Counterfact always includes $.auth.apiKey. If the request omits the key, $.auth.apiKey defaults to an empty string.

Support for other security schemes (OAuth 2, OpenID Connect, mutual TLS) is planned. Open an issue to help prioritize.

Simulating latency: $.delay()

Counterfact responds much faster than a real server. To test loading states and timeouts, use $.delay():

// pause for exactly one second
await $.delay(1000);

// pause for a random duration between 1 and 5 seconds
await $.delay(1000, 5000);

Escaping the type system: $.x

Counterfact translates your OpenAPI spec into strict TypeScript types. If your spec is incomplete, or you need to return something outside the spec (like a 500 error that isn’t documented), the strict types can get in the way.

$.x is an alias for $ with all types widened to any, giving you an escape hatch:

import type { Routes } from "../types/index.js";

type Route = Routes["/pets"];

export const GET: Route["GET"] = function ($) {
  // header not defined in OpenAPI spec
  $.headers["my-undocumented-header"]; // TypeScript error
  $.x.headers["my-undocumented-header"]; // ok

  // status code not defined in OpenAPI spec
  return $.response.status(500).text("Error!"); // TypeScript error
  return $.x.response.status(500).text("Error!"); // ok
};

Streaming responses (SSE, JSONL, JSON-seq)

Counterfact supports OpenAPI 3.2 sequential media types. When your spec uses itemSchema on a text/event-stream, application/jsonl, or application/json-seq content type, the generated handler type expresses the body as AsyncIterable<T>.

Use $.response.status(<status>).stream(iterable) to return a stream from any route handler. The content type is inferred automatically from the OpenAPI spec (the first streaming media type defined for that response).

import type { Routes } from "../types/index.js";

type Route = Routes["/events"];

export const GET: Route["GET"] = function ($) {
  return $.response.status(200).stream(events());
};

async function* events() {
  yield { id: 1, message: "hello" };
  await new Promise((resolve) => setTimeout(resolve, 500));
  yield { id: 2, message: "world" };
}

Each item yielded by the iterable is serialised automatically:

Content typeWire format per item
text/event-streamdata: <json>\n\n
application/json-seq\x1e<json>\n
JSONL / ndjson<json>\n

For text/event-stream responses, Counterfact also sets Cache-Control: no-cache and X-Accel-Buffering: no automatically.

See also

Multipart form requests

For OpenAPI 3 operations with a multipart/form-data request body, generated handlers read form fields from $.body. Required and optional fields follow the schema, including referenced schemas and versioned APIs. JSON and URL-encoded request bodies keep their existing runtime parsing.

A type: string, format: binary multipart property is a MultipartFile:

PropertyValue
dataUint8Array containing the complete uploaded bytes
filenameClient-supplied filename
contentTypeClient-supplied media type
sizeByte length of data

For example, given a required name string and an optional files array whose items have type: string, format: binary, a handler can read $.body.name and $.body.files?.[0]?.data. Treat filenames and media types as untrusted input. Uploads are kept in memory and are never saved to disk automatically.

Send array fields as repeated parts with the same name. A single part for a schema-declared array becomes a one-item array. Repeated scalar fields remain an array and fail request validation. Numeric and boolean fields are decoded from JSON scalar text (3, false); object fields accept JSON object text. Strings remain strings. Invalid values are rejected when request validation is enabled. Custom OpenAPI multipart encoding serialization styles are not interpreted.

The built-in Koa server limits the entire multipart request, including files and part headers, to 10 MiB and returns HTTP 413 when it exceeds that limit. Malformed multipart bodies return HTTP 400 even when request validation is disabled. Missing required fields and invalid file or field types return HTTP 400 under the normal request-validation settings. The original multipart bytes are preserved for $.proxy(url) forwarding.

For multipart unions, already-valid values are preserved. Otherwise Counterfact tries decoded branch candidates in declaration order and selects the first that satisfies the complete schema, including oneOf exclusivity. Object fields with properties or additionalProperties can contain JSON object text even when type is omitted. Nullable binary type arrays expose MultipartFile or null.

Request-body validation selects the declared exact media type, then subtype and full wildcard entries. A present body with an undeclared media type returns 400 before the handler runs.