API Versioning

API versioning represents a critical challenge in the lifecycle of any web service that seeks to evolve continuously without breaking existing client integrations. As business requirements change, bugs are fixed, and new features are added, the API must evolve in a way that enables innovation without forcing all consumers to update simultaneously. An inadequate versioning strategy can result in catastrophic scenarios where seemingly harmless changes break already-distributed mobile applications that cannot be force-updated, partner systems that depend on specific API contracts, or business-critical integrations that process financial transactions. The problem becomes even more complex in microservices architectures, where multiple interdependent APIs must evolve in a coordinated manner, and in public APIs where thousands of third-party developers have built solutions on top of your infrastructure. This article explores the main versioning strategies - including URI versioning, header versioning, and content negotiation - analyzing advantages, disadvantages, and appropriate use cases for each approach, as well as establishing deprecation policies, backward compatibility strategies, and change communication patterns that enable sustainable evolution of the API without compromising the stability of the dependent ecosystem.

Why Version APIs

  • Inevitable breaking changes: Changes to data models, authentication, behavior
  • Heterogeneous clients: Mobile apps, web apps, partners with different update cycles
  • Backward compatibility: Keep older versions working during transition
  • Stable contracts: Ensure predictability for integrators
  • Planned deprecation: Sunset older versions in a controlled manner

Versioning Strategies

1. URI Versioning (Most Common)

      # Version in the URI path
      GET /api/v1/users
      GET /api/v2/users
      # Advantages:
      - Extremely visible and explicit
      - Easy to test different versions
      - Cache-friendly (different URLs)
      - Simple to route in proxies/gateways
      # Disadvantages:
      - Resource duplication (v1/users, v2/users)
      - Can lead to code duplication
      - URL change for the same resource
      

2. Header Versioning

      # Custom header
      GET /api/users
      API-Version: 2.0
      # Accept header (vendor MIME type)
      GET /api/users
      Accept: application/vnd.myapi.v2+json
      # Advantages:
      - URI stays clean and consistent
      - More RESTful (same resource, different representations)
      - Flexibility to version per resource
      # Disadvantages:
      - Less visible (requires header inspection)
      - Makes manual testing harder
      - Complex caching (varies by header)
      

3. Query Parameter Versioning

      # Query string
      GET /api/users?version=2
      GET /api/users?api-version=2.0
      # Advantages:
      - Easy to add to existing requests
      - Keeps the base URI stable
      - Simple for HTTP clients
      # Disadvantages:
      - Can pollute query parameters
      - Less semantic (version is not a filter)
      - Routing/caching problems
      

4. Content Negotiation

      # Media type versioning
      GET /api/users
      Accept: application/vnd.company.user-v2+json
      # Schema versioning
      POST /api/users
      Content-Type: application/vnd.company.user.v2+json
      # Advantages:
      - More RESTful and HTTP-compliant
      - Allows versioning request/response separately
      - Granularity per resource type
      # Disadvantages:
      - Implementation complexity
      - Requires understanding of HTTP content negotiation
      - Harder debugging
      

Semantic Versioning for APIs

      # MAJOR.MINOR.PATCH (Semver adapted for APIs)
      MAJOR: Breaking changes
      - Remove endpoints
      - Change response structure
      - Alter authentication
      - Example: v1.0.0 → v2.0.0
      MINOR: Backward-compatible additions
      - New endpoints
      - New optional fields in responses
      - New optional query parameters
      - Example: v2.0.0 → v2.1.0
      PATCH: Bug fixes
      - Fixes without contract changes
      - Performance improvements
      - Example: v2.1.0 → v2.1.1
      # Communication
      GET /api/v2/info
      {
      "version": "2.3.1",
      "deprecatedAt": "2025-06-01",
      "sunsetAt": "2025-12-01"
      }
      

Backward Compatibility

Backward-Compatible Changes

  • [OK] Add new endpoints
  • [OK] Add optional fields in requests
  • [OK] Add new fields in responses (clients should ignore them)
  • [OK] Make required fields optional
  • [OK] Add new values to existing enums
  • [OK] Relax validations (accept more inputs)

Breaking Changes (Require a New Version)

  • [X] Remove or rename endpoints
  • [X] Remove or rename fields in responses
  • [X] Change data types (string → number)
  • [X] Add required fields in requests
  • [X] Tighten validations (reject previously accepted inputs)
  • [X] Change authentication/authorization behavior

Deprecation Policy

      # 1. Deprecation Announcement (6-12 months in advance)
      {
      "data": [...],
      "deprecated": true,
      "deprecation": {
      "date": "2025-01-01",
      "sunset": "2025-07-01",
      "alternativeVersion": "v3",
      "migrationGuide": "https://docs.api.com/migrate-v2-to-v3"
      }
      }
      # 2. Deprecation Headers
      Deprecation: true
      Sunset: Wed, 01 Jul 2025 00:00:00 GMT
      Link: <https://docs.api.com/migrate>; rel="deprecation"
      # 3. Usage Monitoring
      - Log requests per version
      - Identify clients still using deprecated versions
      - Proactive notifications to developers
      # 4. Overlap Period
      v2 Launch ─────────────────────────────►
      v3 Launch ─────────────►
      v2 Deprecated ─────►
      v2 Sunset
      

Implementation with Express.js

      // Router-based versioning
      const express = require('express');
      const app = express();
      // V1 routes
      const v1Router = express.Router();
      v1Router.get('/users', (req, res) => {
      res.json({ version: 'v1', users: [...] });
      });
      app.use('/api/v1', v1Router);
      // V2 routes
      const v2Router = express.Router();
      v2Router.get('/users', (req, res) => {
      res.json({
      version: 'v2',
      users: [...],
      metadata: { ... }  // New in v2
      });
      });
      app.use('/api/v2', v2Router);
      // Header-based versioning
      app.get('/api/users', (req, res) => {
      const version = req.headers['api-version'] || '1';
      if (version === '2') {
      return res.json({ version: 'v2', users: [...] });
      }
      res.json({ version: 'v1', users: [...] });
      });
      

GraphQL Versioning

      # GraphQL does not need traditional versioning
      # Use schema evolution and the @deprecated directive
      type User {
      id: ID!
      name: String!
      email: String!
      username: String! @deprecated(reason: "Use 'name' field instead")
      }
      # Field-level deprecation
      type Query {
      users: [User!]!
      getUsers: [User!]! @deprecated(reason: "Use 'users' query instead")
      }
      # Additive changes are naturally backward-compatible
      # Clients request only the fields they know
      

Best Practices

  • Choose one strategy and be consistent
  • Document versioning policies clearly
  • Use semantic versioning to communicate the impact of changes
  • Keep at least 2 versions active simultaneously
  • Implement deprecation warnings in responses
  • Provide detailed migration guides
  • Monitor usage metrics per version
  • Automate cross-version tests
  • Communicate changes in advance (changelog, emails)
  • Consider clients that cannot update quickly

Final Recommendation

For public and long-lived APIs, URI versioning (/api/v1/) is generally the best choice due to its clarity and ease of use. Combine it with semantic versioning to communicate the impact of changes. For internal microservices APIs, consider header versioning for greater flexibility. Always maintain backward compatibility when possible and establish clear deprecation policies with generous transition periods (6-12 months minimum).