Files
cfw-autumn/.claude/skills/api-versioning/references/version-change-anatomy.md
2026-01-16 09:16:26 +00:00

5.2 KiB

Version Change Anatomy

Deep dive into each field of defineVersionChange().

Full Configuration

export const V1_2_CustomerChange = defineVersionChange({
  // Required fields
  newVersion: ApiVersion.V2_0,
  oldVersion: ApiVersion.V1_Beta,
  description: ["Change 1", "Change 2"],
  affectedResources: [AffectedResource.Customer],
  newSchema: ApiCustomerSchema,
  oldSchema: ApiCustomerV3Schema,

  // Optional fields
  name: "V1_2_CustomerChange",  // For debugging
  legacyDataSchema: CustomerLegacyDataSchema,
  affectsRequest: false,   // Default: false
  affectsResponse: true,   // Default: true
  hasSideEffects: false,   // Default: false

  // Transform functions
  transformResponse: ({ input, legacyData, ctx }) => { ... },
  transformRequest: ({ input, legacyData }) => { ... },
});

Version Fields

newVersion

The version where the breaking change was introduced.

newVersion: ApiVersion.V2_0  // V2.0 introduced this breaking change

oldVersion

The version we're transforming TO. Transform is applied when targetVersion <= oldVersion.

oldVersion: ApiVersion.V1_Beta  // We transform TO V1.2 format

Example: If user is on V1.1 and oldVersion is V1.2, this change IS applied (V1.1 <= V1.2).

Schema Fields

newSchema

Zod schema for the latest/newer format. Input to transformResponse() must match this.

oldSchema

Zod schema for the older format. Output from transformResponse() is validated against this.

legacyDataSchema (Optional)

Schema for legacy fields that were removed in newer versions but need to be reconstructed for older versions.

// Customer had product_ids in V1.2, removed in V2.0
export const CustomerLegacyDataSchema = z.object({
  cusProductLegacyData: z.record(CusProductLegacyDataSchema),
  cusFeatureLegacyData: z.record(CusFeatureLegacyDataSchema),
});

Behavior Flags

affectsRequest

Set true if this change transforms incoming requests (old → new).

affectsRequest: true  // transformRequest() will be called

affectsResponse

Set true if this change transforms outgoing responses (new → old). Default: true.

affectsResponse: true  // transformResponse() will be called

hasSideEffects

Set true for changes that affect behavior, not data shape. Transforms become no-ops.

hasSideEffects: true  // transformResponse() is skipped; handle logic elsewhere

Use with backwardsChangeActive() in handlers:

if (backwardsChangeActive({ apiVersion: ctx.apiVersion, versionChange: V0_2_InvoicesAlwaysExpanded })) {
  expand.push(CusExpand.Invoices);
}

Transform Functions

transformResponse

Called for response transformations (new → old).

transformResponse: ({ input, legacyData, ctx }) => {
  // input: Data in newSchema format (latest)
  // legacyData: Data from legacyDataSchema (optional)
  // ctx: { features: Feature[] } - runtime context

  return {
    // Return data in oldSchema format
  } satisfies z.infer<typeof ApiCustomerV3Schema>;
}

transformRequest

Called for request transformations (old → new).

transformRequest: ({ input, legacyData }) => {
  // input: Data in oldSchema format (user's request)
  // legacyData: Data from legacyDataSchema (optional)

  return {
    // Return data in newSchema format (latest)
  } satisfies z.infer<typeof GetCustomerQuerySchema>;
}

affectedResources

Specifies which resources this change applies to. Used by applyResponseVersionChanges() to filter changes.

affectedResources: [AffectedResource.Customer]

Available resources (from VersionChange.ts):

enum AffectedResource {
  Customer = "customer",
  Entity = "entity",
  CusProduct = "cus_product",
  CusFeature = "cus_feature",
  CusBalance = "cus_balance",
  Invoice = "invoice",
  Product = "product",
  Feature = "feature",
  Check = "check",
  Track = "track",
  Checkout = "checkout",
  Attach = "attach",
  ApiSubscriptionUpdate = "api_subscription_update",
}

Version Context

The ctx parameter provides runtime data needed for transformations:

interface VersionContext {
  features: Feature[];  // Organization's features
}

Used when transform needs to look up feature data:

transformResponse: ({ input, ctx }) => {
  const feature = ctx.features.find(f => f.id === input.feature_id);
  return {
    feature_name: feature?.name ?? null,
    // ...
  };
}

Validation Behavior

Response Transform Output

Output is validated with safeParse():

  • If validation succeeds: Extra fields are stripped, cleaned data returned
  • If validation fails: Original (unvalidated) data returned for graceful degradation

Compile-Time Excess Property Checking

TypeScript allows excess properties in object spreads. Use satisfies for compile-time errors:

return {
  id: input.id,
  oldField: input.newField,
} satisfies z.infer<typeof OldSchema>;  // Compile error if shape wrong

For stricter runtime validation, use schema.strict():

oldSchema: ApiCustomerV3Schema.strict()  // Runtime error on extra fields