Multiple API Versions

Serve multiple API versions from one project, sharing the handlers and branching only where the contract actually differs.

Why teams use this

Versioned APIs introduce change gradually: a new field in the response, a renamed parameter, a removed endpoint. Duplicating every handler for every version creates a maintenance burden and lets the versions drift. You need a way to share as much handler logic as possible across versions while still making version-specific adjustments in the places that actually changed.

How it works

List each versioned spec under the spec key in counterfact.yaml. Give them the same group and different version labels. Counterfact generates a shared route file per path and injects two helpers into the handler’s $ argument at runtime:

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

Write one handler that branches on version using $.minVersion() instead of duplicating the file.

Example

Configuration

# counterfact.yaml
spec:
  - source: ./api-v1.yaml
    group: pets
    version: v1
  - source: ./api-v2.yaml
    group: pets
    version: v2
  - source: ./api-v3.yaml
    group: pets
    version: v3
# Handlers are served at:
#   http://localhost:3100/pets/v1/...
#   http://localhost:3100/pets/v2/...
#   http://localhost:3100/pets/v3/...

Handler

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

export const GET: HTTP_GET = ($) => {
  const pet = $.context.getById($.path.petId);
  if (!pet) return $.response[404].text("Pet not found");

  // v1 returns only id and name
  if (!$.minVersion("v2")) {
    return $.response[200].json({ id: pet.id, name: pet.name });
  }

  // v2 adds the status field
  if (!$.minVersion("v3")) {
    return $.response[200].json({
      id: pet.id,
      name: pet.name,
      status: pet.status,
    });
  }

  // v3 adds the full pet object including photoUrls
  return $.response[200].json(pet);
};

$.minVersion("v2") returns true for requests handled by v2 and v3, and false for v1. The conditions layer naturally: the last return in the example only runs when v3 or later is handling the request.

TypeScript narrowing

$.minVersion() is a type predicate. After a passing check, TypeScript narrows $ to the intersection of only the versions that satisfy the minimum, giving you accurate autocompletion and type errors for version-specific fields:

export const GET: HTTP_GET = ($) => {
  if ($.minVersion("v2")) {
    // $ is now typed as the v2 (or v3, v4, …) $ type
    // v2-only fields are available here
  }
};

What you get

Keep exploring