Zuplo
API Versioning

API Backwards Compatibility Best Practices

Adrian MachadoAdrian Machado
April 11, 2025
8 min read

Learn how to maintain backward compatibility in API versioning through best practices like semantic versioning and thorough documentation.

API Versioning ensures your software evolves without breaking existing integrations. Here’s how to maintain backward compatibility while rolling out new features:

  • Versioning Methods: Choose between URI paths (/v1), custom headers (api-version: 1.0), or query parameters (?version=1.0).
  • Additive Changes: Add new fields or endpoints instead of altering existing ones.
  • Thorough Testing: Automate testing for contract compliance, integration, and version comparisons.
  • Documentation: Provide changelogs, migration guides, and accurate API specs.
  • Deprecation Planning: Notify users 6–12 months in advance and allow gradual migration.

Quick Tip: Tools like API gateways simplify version management with features like OpenAPI support to manage documentation of different versions, version-based routing, and auto-syncing developer portals that keep your users in the loop on changes..

Want to avoid breaking your API users’ trust? Stick to these practices to keep your APIs stable while introducing updates.

Making APIs Backward Compatible

Ensuring backward compatibility is all about careful planning to keep existing integrations intact while rolling out new features.

Version Naming Methods

As covered in our API Versioning guide here are some common strategies:

MethodImplementationBenefitsConsiderations
URI Path/v1/resourcesEasy to understandMay make URLs longer
Custom Headersapi-version: 1.0Keeps URLs cleanRequires header parsing
Query Parameters?version=1.0Simple for testingLess aligned with REST

As discussed in our Github API versioning mistakes article - header-based versioning is usually the one most poorly implemented when considering backwards compatibility. Make sure that your enforce the user providing a version header rather than assuming they always want the latest version when its omitted. Same advice applies to query parameter versioning as well.

Semantic Version Numbers

Semantic versioning (MAJOR.MINOR.PATCH) is a clear way to communicate API updates. You wouldn’t use it in an API path, but either header or query based versioning can make use of this pattern. Each part of the version number represents a specific type of change:

Version ComponentChange TypeExampleImpact
MAJOR (X.0.0)Breaking changes2.0.0Incompatible API updates
MINOR (1.X.0)New features1.1.0Compatible with older versions
PATCH (1.0.X)Bug fixes1.0.1Compatible fixes

Check out our full guide to semantic versioning for more info.

Adding Features Safely

Introduce new features without causing disruptions by following these best practices:

  • Start with Feature Flags
    Use feature flags to control how and when new features are rolled out. This approach allows for gradual deployment and quick rollbacks if something goes wrong. For example, when adding new fields to a response, hide them behind a flag initially.

  • Stick to Additive Changes
    Add new fields or endpoints instead of altering existing ones. This ensures older clients keep working while newer ones can access additional features.

  • Keep Response Structures Consistent
    Maintain a predictable response format by following these rules:

    Do ✓Don’t ✗
    Add optional fieldsRemove existing fields
    Extend arrays/objectsChange field types
    Add new endpointsModify URL structures

Technical Solutions for Compatibility

Focus on thorough testing and efficient management to ensure API backward compatibility.

Testing for Breaking Changes

Automated testing in CI/CD pipelines is a must for catching compatibility issues before they make it to production. Here’s a breakdown of effective testing methods:

Testing LayerPurposeImplementation
Contract TestingChecks API spec complianceOpenAPI specification validation
Integration TestingEnsures client compatibilityTest against various client versions
Version ComparisonIdentifies breaking changesAutomated diff analysis

Every code change should trigger automated tests to confirm:

  • Response structures remain consistent, and required fields are intact
  • Compatibility with earlier API versions
  • Data types are consistent
  • Endpoint behaviors stay reliable

This level of testing ensures smooth version management and effective routing.

API Gateway Benefits

API gateways play a key role in managing versions and ensuring compatibility. They simplify client routing and enforce version integrity after testing.

Take Zuplo’s programmable API gateway as an example. It offers tools to maintain compatibility while reducing complexity:

  • OpenAPI Native
    Keeps your API gateway configuration aligned with the latest design, avoiding spec-drift. Users typically create a new OpenAPI document per version in order to maintain backwards compatibility. These documents are then cataloged by the autogenerated developer portal.
  • Version Routing
    Built-in routing support for path and header based versioning - with a programmable override if you want to do dynamic routing.
  • Custom Version Management
    Developers can create custom logic for version management using extensible policies, tailoring the solution to specific requirements without compromising compatibility.

Change Management for APIs

Managing changes effectively ensures API users stay informed and can adjust without disruptions.

Writing Clear Documentation

Documentation acts as the bridge between API providers and users. It should include changelogs, migration guides, and accurate API specifications:

Documentation TypePurposeKey Components
ChangelogsRecord version updatesVersion number, date, changes, impacts
Migration GuidesHelp users transition versionsStep-by-step instructions, code examples
API SpecificationsDescribe current endpointsOpenAPI/Swagger specs, request/response schemas

When updating documentation, focus on:

  • Impact Assessment: Clearly identify affected endpoints or features.
  • Code Examples: Show before-and-after examples to guide users.
  • Version Differences: Highlight specific changes between versions.
  • Breaking Changes: Clearly flag any updates requiring client modifications.

Using OpenAPI ensures your documentation stays aligned with the API’s implementation [1]. This approach creates a solid foundation for managing updates effectively.

Update and End-of-Life Planning

Planning version updates and retirements helps users prepare for changes. Set clear timelines for:

1. Version Deprecation Notice

Notify users 6–12 months in advance about version deprecation.

2. Sunset Schedule

Provide a timeline that outlines:

  • Release dates for new versions.
  • Support duration for older versions.
  • Final cutoff dates for deprecated versions.

3. Migration Windows

Define transition periods where both old and new versions operate simultaneously. This allows users to migrate gradually without affecting their services.

A developer portal integrated with your API can simplify this process. It offers users self-service access to:

  • Current API status and health.
  • Version-specific documentation.
  • Usage analytics.
  • API key management.
  • Rate limiting details.

Having a centralized developer portal ensures users can access everything they need in one place, making transitions smoother.

If you’d like a more technical walkthrough of API deprecation, check out our API deprecation guide

Routing Configuration for API Versioning

The practices above focus on what to do when versioning your API, but the question of where to manage version routing is equally important. Handling version resolution at the API gateway layer rather than scattering it across individual services keeps your backend code clean and your versioning strategy consistent.

URL-Based Versioning with Gateway Routes

URL path versioning is the most widely adopted strategy for public APIs because it is explicit and easy to discover. When you manage versioned routes through an API gateway like Zuplo, your routes.oas.json file acts as a single source of truth for both routing and documentation. Here is an example showing v1 and v2 of a resource endpoint:

JSONjson
{
  "paths": {
    "/v1/products": {
      "get": {
        "operationId": "list-products-v1",
        "summary": "List products (v1 - deprecated)",
        "x-zuplo-route": {
          "handler": {
            "export": "default",
            "module": "$import(./modules/v1/products)",
            "options": {}
          },
          "policies": {
            "inbound": ["api-key-auth"],
            "outbound": ["v1-deprecation-headers"]
          }
        }
      }
    },
    "/v2/products": {
      "get": {
        "operationId": "list-products-v2",
        "summary": "List products (v2)",
        "x-zuplo-route": {
          "handler": {
            "export": "default",
            "module": "$import(./modules/v2/products)",
            "options": {}
          },
          "policies": {
            "inbound": ["api-key-auth"],
            "outbound": []
          }
        }
      }
    }
  }
}

Each route version gets its own operationId, handler module, and policy pipeline. This means you can direct v1 traffic to a legacy service and v2 traffic to an entirely different implementation without any conditional logic inside the handlers themselves. The route file is also the foundation for the auto-generated developer portal, so consumers always see accurate, per-version documentation.

Deprecation Headers with an Outbound Policy

One of the strongest signals you can send to API consumers is the combination of Deprecation, Sunset, and Link headers on responses from older versions. Instead of adding this logic to every backend service, you can implement it once as a reusable outbound policy in your gateway. The following TypeScript example shows how:

TypeScripttypescript
import { ZuploContext, ZuploRequest, HttpProblems } from "@zuplo/runtime";

export default async function addDeprecationHeaders(
  response: Response,
  request: ZuploRequest,
  context: ZuploContext,
) {
  // Clone the response so we can modify headers
  const headers = new Headers(response.headers);

  // RFC 8594 Deprecation header — signals this version is deprecated
  headers.set("Deprecation", "true");

  // Sunset header — the date after which this version may stop working
  headers.set("Sunset", "Sat, 30 Jun 2026 23:59:59 GMT");

  // Link header — direct consumers to the replacement version
  headers.set(
    "Link",
    '<https://api.example.com/v2/products>; rel="successor-version"',
  );

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers,
  });
}

The Deprecation: true header follows RFC 8594 and tells clients that this version is officially deprecated. The Sunset header communicates the exact date when the endpoint will be decommissioned, giving consumers a concrete deadline. The Link header with rel="successor-version" provides a machine-readable pointer to the replacement, enabling automated migration tooling and developer dashboards to surface upgrade paths without manual intervention. Because this policy is attached at the route level, you can add or remove it from any endpoint through a configuration change rather than a code deployment.

Why Gateway-Level Versioning Matters

Managing versioning at the gateway rather than inside your services offers several concrete benefits. It centralizes all version routing into a single, declarative configuration file, which means every team follows the same versioning conventions without reimplementing routing logic in each service. Policies like the deprecation header example are written once and applied to any number of endpoints, ensuring consistency and reducing the surface area for mistakes. The entire configuration is stored in version control, so every change to your versioning strategy is auditable through your standard code review process. Additionally, because the gateway is the entry point for all traffic, it is the natural place to collect per-version analytics that inform decisions about when to sunset older endpoints and how to allocate support resources during migration windows.

Conclusion

Main Takeaways

Maintaining backward compatibility requires striking the right balance between introducing new features and ensuring stability. Key practices to achieve this include:

  • Requiring Explicit Versions from users ideally in the semver format
  • Implementing thorough testing to catch potential issues early
  • Providing detailed documentation and migration guides for developers
  • Planning version lifecycles to manage updates effectively
  • Allowing adequate transition periods to minimize disruption

These steps help ensure APIs evolve smoothly while keeping both developers and end-users in mind. If you’re looking to release a new version of your API you’ll need an API gateway tool to manage the transition. Sign up for a free Zuplo account today and discover how easy versioning can be with native OpenAPI support, version-based routing, gitops, breaking change detection, developer portal auto-generation, and more!