Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

API Versioning and Backward Compatibility

API Versioning and Backward Compatibility

~6 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Chapters 79-84 firmly established four contracts: RewardInterface, RewardRepositoryInterface, the webapi.xml routes, and schema.graphqls. Once external clients - a mobile app, a partner system, another team - build against these contracts, every later change to them becomes a backward-compatibility question. This chapter deliberately stays conceptual instead of building new code - it sets the rules every future change to this module has to follow.

The uncomfortable truth about PHP interfaces

Extending a class with a new method is backward compatible - existing code using the class notices nothing. For an interface, the opposite is true: adding a new abstract method getStockStatus(): string to RewardInterface forces PHP to require every implementation to add it - including third-party classes outside this project that implement RewardInterface themselves (say, an alternative Model\Data\Reward via their own preference). A breaking change, disguised as a seemingly harmless addition.

Achtung: That's exactly why RewardInterface deliberately extends ExtensibleDataInterface in chapter 79: getExtensionAttributes()/setExtensionAttributes() are Magento's own sanctioned, safe extension path. A new field goes into extension_attributes.xml instead of being added directly as a new interface method - existing implementations stay valid unchanged, and the code generator simply rebuilds RewardExtensionInterface.

When a V2 is actually needed

  • Never needed for additive REST changes: a new, optional response field via extension attributes leaves existing V1 clients untouched - they simply ignore what they don't know about.
  • Never needed to remove a field nobody uses anymore - provided that can actually be confirmed from real access logs, which is rarely the case for a public API.
  • Genuinely needed as soon as the meaning of an existing field changes (example below), a required parameter gets added, or a method's return type changes incompatibly.

A concrete, hypothetical example of the last case: if points_per_euro (chapter 7) were to eventually vary by product category, and PointsSummaryInterface::getPointsBalance() therefore had to return an entire breakdown instead of a plain points count - a genuine meaning change, not an additive field. The clean path would then be a dedicated Api\PointsManagementV2Interface with a new getPointsSummaryV2() method, registered under a second webapi.xml route:

<route url="/V2/loyalty/points/mine" method="GET">
    <service class="Mironsoft\Loyalty\Api\PointsManagementV2Interface" method="getPointsSummary"/>
    <resources>
        <resource ref="self"/>
    </resources>
    <data>
        <parameter name="customerId" force="true">%customer_id%</parameter>
    </data>
</route>

/V1/loyalty/points/mine from chapter 80 would stay in place unchanged - existing clients don't break, new clients migrate to /V2 voluntarily once they're ready. Purely illustrative: PointsManagementV2Interface isn't actually built in this module, the example only shows the pattern.

GraphQL versions itself differently from REST

GraphQL has no /V2 URL prefix - a schema keeps evolving continuously instead of being cut into versions. The official GraphQL path for a meaning change is the @deprecated directive: the old field keeps working but is marked deprecated and becomes visible to tooling through introspection (chapter 87).

type LoyaltyReward @doc(description: "A single redeemable reward") {
    reward_id: Int!
    identifier: String!
    title: String!
    is_active: Boolean! @deprecated(reason: "Use reward_status instead, which distinguishes active/paused/archived")
    reward_status: String @doc(description: "One of active, paused, archived")
}

Purely illustrative here too - reward_status doesn't actually exist in this module, is_active from chapter 82 remains this series' actual state. The example only shows what a later, real meaning change to LoyaltyReward would look like without immediately breaking existing clients.

A checklist for every future change

  1. Can the change be modeled as a new, optional field via extension attributes (REST) or an additive schema extension (GraphQL)? Then no new version is needed.
  2. Does the meaning of an existing field change, not just its presence? Then a V2 route (REST) or @deprecated plus a new field (GraphQL).
  3. Is a method being removed from an existing Api interface, or its signature changed? Practically never without a new version - PHP otherwise forces an error on every third-party implementation.
  4. Is the change documented (chapter 87) and recorded in this series' spec document, so later blocks know the same contract?

With clear stability rules in place, chapter 86 turns to a different threat: what happens when a client knows the rules but simply calls the redemption route from chapter 81 too often, too fast?