Files
cfw-autumn/shared/api/versionUtils

API Versioning System

Stripe-inspired versioning with CalVer (external) and SemVer (internal).

Core Concept

Always build latest format → Transform backwards automatically

// Your handler
const latestData = { features: { f1: {...} } };  // V1_2 format

return applyVersionChanges({
  data: latestData,
  currentVersion: new ApiVersionClass(LATEST_VERSION),
  targetVersion: ctx.apiVersion,  // From middleware
  resource: AffectedResource.Customer
});

Quick Reference

Version Comparison

// ctx.apiVersion ready in middleware
if (ctx.apiVersion.gte(ApiVersion.V1_1)) { ... }
if (ctx.apiVersion.lt(ApiVersion.V1_2)) { ... }

Version Mapping

SemVer CalVer Legacy v1 Legacy v2
V1_4 2025-06-01 - 1.4 (beta)
V1_2 2025-05-05 - 1.2
V1_1 2025-04-17 - 1.1
V0_2 2025-04-01 0.2 1.0
V0_1 2025-02-01 0.1 -

Side Effects

if (ctx.apiVersion.lt(ApiVersion.V1_1)) {
  expandArray.push(CusExpand.Invoices);
}

How Transforms Work

  1. User requests V1_1, your data is V1_2:

    V1_2 { features: { f1: {...} } }
       ↓ FeaturesArrayToObject.transform()
    V1_1 { features: [{ feature_id: 'f1', ...}] }
    
  2. Multiple versions back (V1_2 → V0_2):

    V1_2 → V1_1 → V0_2
    (Each transform applied in sequence)
    

File Structure

shared/api/
├── versionUtils/
│   ├── ApiVersion.ts              # Version enum
│   ├── ApiVersionClass.ts         # Comparison methods
│   ├── versionRegistry.ts         # SemVer ↔ CalVer mappings
│   ├── versionRegistryUtils.ts    # Helper functions
│   ├── convertVersionUtils.ts     # Conversion utils
│   ├── versionBranchUtils.ts      # Branching helpers
│   └── versionChangeUtils/
│       ├── VersionChange.ts              # Abstract base
│       ├── VersionChangeRegistryClass.ts # Registry class
│       ├── versionChangeRegistry.ts      # Register all changes
│       └── applyVersionChanges.ts        # Transform engine
└── customers/
    └── changes/                   # Customer-specific changes
        ├── V1_2_FeaturesArrayToObject.ts
        ├── V1_1_MergedResponse.ts
        ├── V1_1_LegacyExpandInvoices.ts  # Side effect
        └── V0_2_ProductItems.ts

Change Organization

Version changes live with the resource they affect:

  • Customer changes → shared/api/customers/changes/
  • Product changes → shared/api/products/changes/
  • Invoice changes → shared/api/invoices/changes/

Naming convention: V{version}_{Description}.ts

  • V1_2_FeaturesArrayToObject.ts
  • V1_1_MergedResponse.ts

Creating Version Changes

1. Create Change Class

// shared/api/customers/changes/V1_3_MyChange.ts
import { ApiVersion, VersionChange, AffectedResource } from "@autumn/shared";

export class V1_3_MyChange extends VersionChange {
  readonly version = ApiVersion.V1_3;
  readonly description = "Brief description";
  readonly affectedResources = [AffectedResource.Customer];

  transform({ data }: { data: any }): any {
    // Transform FROM V1_3 TO V1_2
    return { ...data, oldField: data.newField };
  }
}

2. Register in Registry

// versionChangeUtils/versionChangeRegistry.ts
export const V1_3_CHANGES = [
  V1_3_MyChange,
  V1_3_AnotherChange,
];

export function registerAllVersionChanges() {
  VersionChangeRegistryClass.register({
    version: ApiVersion.V1_3,
    changes: V1_3_CHANGES
  });
  // ... other versions
}

Side Effect Changes

export class V1_3_MySideEffect extends VersionChange {
  readonly hasSideEffects = true;  // Mark as side effect
  // ... rest
}

// In handler:
if (ctx.apiVersion.lt(ApiVersion.V1_3)) {
  // Handle side effect logic
}

CalVer with .clover Support

System supports .clover suffix for non-breaking changes:

  • 2025-04-17 → Breaking change
  • 2025-04-17.clover → Non-breaking update (future use)

Both map to same SemVer internally.

Usage Patterns

Standard Handler

export const handleGet = createRoute({
  handler: async (c) => {
    const ctx = c.get("ctx");

    // Build latest
    const data = buildLatest();

    // Transform
    return c.json(applyVersionChanges({
      data,
      currentVersion: new ApiVersionClass(LATEST_VERSION),
      targetVersion: ctx.apiVersion,
      resource: AffectedResource.Customer
    }));
  }
});

With Version Logic

// Check version
if (ctx.apiVersion.lt(ApiVersion.V1_1)) {
  expandArray.push(CusExpand.Invoices);
}

// Use helper
const withItems = ctx.apiVersion.gte(ApiVersion.V0_2);

Middleware

apiVersionMiddleware resolves version from:

  1. x-api-version header (CalVer: "2025-04-17")
  2. org.api_version (legacy: 1.1)
  3. org.config.api_version (legacy: 0.2)
  4. Default: V0_2

Result stored in ctx.apiVersion (ApiVersionClass).

Migration from Old System

Before

const apiVersion = orgToVersion({ org, reqApiVersion });
if (apiVersion >= LegacyVersion.v1_1) { ... }

After

if (ctx.apiVersion.gte(ApiVersion.V1_1)) { ... }

Key Principles

  1. Always build latest - Let transforms handle old versions
  2. Transforms go backwards - New → Old, never Old → New
  3. Object parameters - All functions use { param } signature
  4. Descending order - Version lists newest first
  5. Resource organization - Changes live with affected resources