Zuplo
API Versioning

API Versioning & Backward Compatibility: Best Practices

Adrian MachadoAdrian Machado
April 11, 2025
8 min read

Learn API versioning best practices for backward compatibility, including semantic versioning, RFC 8594 Sunset headers, and gateway-level version routing.

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:

Method Implementation Benefits Considerations
URI Path /v1/resources Easy to understand May make URLs longer
Custom Headers api-version: 1.0 Keeps URLs clean Requires header parsing
Query Parameters ?version=1.0 Simple for testing Less 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 you enforce the user providing a version header rather than assuming they always want the latest version when it’s 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 Component Change Type Example Impact
MAJOR (X.0.0) Breaking changes 2.0.0 Incompatible API updates
MINOR (1.X.0) New features 1.1.0 Compatible with older versions
PATCH (1.0.X) Bug fixes 1.0.1 Compatible fixes

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

Adding Features Without Breaking Clients

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. For a deeper look at how small design choices prevent breaking changes, see our guide on common pitfalls in RESTful API design.

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

    Do ✓ Don’t ✗
    Add optional fields Remove existing fields
    Extend arrays/objects Change field types
    Add new endpoints Modify URL structures

When deciding between partial updates and full replacements for your resources, understanding the difference between HTTP PATCH and PUT helps you design version-safe endpoints from the start.

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 Layer Purpose Implementation
Contract Testing Checks API spec compliance OpenAPI specification validation
Integration Testing Ensures client compatibility Test against various client versions
Version Comparison Identifies breaking changes Automated 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.

Implementing API Versioning at the Gateway

API gateways play a key role in managing versions and ensuring compatibility. They centralize client routing, enforce version integrity, and keep your backend code free of version-switching logic.

Zuplo’s programmable API gateway, for example, provides 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.

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 } from "@zuplo/runtime";

export default async function addDeprecationHeaders(
  response: Response,
  request: ZuploRequest,
  context: ZuploContext,
) {
  // Create a mutable copy of the response headers
  const headers = new Headers(response.headers);

  // RFC 9745 Deprecation header — signals this version is deprecated
  // Note: "true" is a widely used shorthand; RFC 9745 specifies a date value
  headers.set("Deprecation", "true");

  // Sunset header — the date after which this version may stop working
  headers.set("Sunset", "Sat, 31 Jan 2027 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 header follows RFC 9745 and tells clients that this version is officially deprecated. The Sunset header, defined in RFC 8594, 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.

Centralizing version routing at the gateway rather than inside individual services offers several concrete benefits. It puts all version routing into a single, declarative configuration file, so 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.

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 Type Purpose Key Components
Changelogs Record version updates Version number, date, changes, impacts
Migration Guides Help users transition versions Step-by-step instructions, code examples
API Specifications Describe current endpoints OpenAPI/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. 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

Your API Versioning Checklist

Maintaining backward compatibility requires striking the right balance between introducing new features and ensuring stability. Before you release a new API version, confirm you have addressed each of these practices:

  • 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, developer portal auto-generation, and more!

Try Zuplo free

Try the platform behind this guide

Zuplo is a developer-first API gateway. Deploy your first API in minutes — no credit card required.

  • 100K requests/mo free
  • GitOps deploys
  • 300+ edge locations

Try Zuplo free — 100K requests/mo

Start free