Header-Based Versioning

Lifecycle & Versioning Jan 6, 2025 HTTP

Definition

Imagine you walk into a restaurant where instead of having separate menus for vegetarian, vegan, and regular diners, you simply tell the host your preference and receive the appropriate menu at your table. The restaurant address is the same, the table is the same, but the experience is customized based on your preference. Header-based versioning works the same way - instead of putting version numbers in the URL (like /v1/users or /v2/users), you specify which version you want in the HTTP headers. The URL stays clean and consistent; the version travels as metadata.

This approach appeals to API purists who argue that the URL should identify the resource (what you’re asking for), not the representation (how you want it delivered). In REST philosophy, /users/123 should always mean “user with ID 123” - whether you want version 1 or version 2 of the API response, or JSON or XML format. These are representation choices, not resource identification, so they belong in headers. GitHub adopted this philosophy with their X-GitHub-Api-Version header, allowing developers to request specific API versions while keeping URLs focused on resources.

The practical benefits are significant for complex applications. Your URLs remain stable for documentation, testing, and caching. You can support multiple versions simultaneously without URL proliferation. API clients can be upgraded by changing a single header value rather than rewriting all endpoint URLs. And it plays nicely with tools that rely on URL structure, like caches and load balancers. The tradeoff is discoverability - you can’t tell which version you’re using just by looking at a URL in your browser, and testing requires setting custom headers.

Example

Real-World Scenario 1: GitHub’s API Versioning GitHub uses X-GitHub-Api-Version: 2022-11-28 to specify which version of their REST API you want. If you’re building a CI/CD integration tool, you set this header once in your API client configuration, and all your requests use that version. When GitHub releases a new version with breaking changes to how pull request reviews work, your tool keeps functioning on the pinned version while you test and migrate to the new one.

Real-World Scenario 2: Microsoft Azure Services Azure APIs use the api-version header (e.g., api-version: 2023-05-01) for their services. When you’re managing cloud resources programmatically, you specify the version in headers rather than URLs. This keeps your infrastructure-as-code templates cleaner - the resource URLs are stable identifiers, and the version is configuration that can be changed in one place.

Real-World Scenario 3: Internal Microservices A company with dozens of internal microservices uses header-based versioning for service-to-service communication. When the User Service updates its API, consuming services add Service-Version: 2 to their requests to opt into the new version when ready. The service mesh’s central configuration can enforce version requirements, and monitoring can track which versions are being called.

Real-World Scenario 4: Mobile App Gradual Migration A mobile banking app uses header-based versioning with X-API-Version: 3.1. When the API team releases version 3.2 with new fraud detection responses, they can roll it out gradually: 10% of users get version 3.2 (via feature flags modifying the header), observe for issues, then expand. If problems arise, it’s a configuration change, not an app update. The URLs in the app’s code never change.

Analogy

The Restaurant Preference Card: When you arrive at a fine restaurant, you don’t choose a different restaurant for vegetarian options - you tell the host your dietary preferences, and they bring you the appropriate menu. The restaurant (URL) is the same; your experience (API response) is customized based on what you communicate (header). Header-based versioning is this preference system for APIs.

The Magazine Subscription with Preferences: You subscribe to a magazine at one address, but you specify your edition preference - print or digital, full version or condensed, large print or regular. The subscription address stays the same; your preferences travel with your subscription. Changing from print to digital doesn’t require a new mailing address. Header-based versioning works identically.

The Universal Remote Control: A universal remote controls your TV, soundbar, and streaming box from one device. You don’t need different remotes (different URLs) - you select which device mode you want (version header) and the same buttons do different things. The remote (API endpoint) is universal; the mode (version) determines behavior.

The Language Line on Phone Support: When you call a company’s support number, you don’t dial different numbers for different languages - you select your language preference from the menu. The phone number (URL) is the same; your language preference (header) customizes the experience. Adding support for a new language doesn’t require publishing a new phone number.

Code Example


// Header-based versioning (custom header)
GET https://api.company.com/customers/123
X-API-Version: 2

// Same URL, different version
GET https://api.company.com/customers/123
X-API-Version: 1

// Azure API Management style (query parameter in header)
GET https://api.company.com/customers/123
api-version: 2023-05-01

// GitHub style (date-based version in header)
GET https://api.github.com/repos/octocat/hello-world
X-GitHub-Api-Version: 2022-11-28

// Response confirms which version was used
[HTTP/1.1](https://reference.apios.info/terms/http-1-1/) 200 OK
X-API-Version: 2
Content-Type: application/json

{
  "id": 123,
  "name": "John Doe",
  "email": "[email protected]"
}

// Server implementation (Express.js)
app.use((req, res, next) => {
  // Default to latest version if not specified
  const version = req.headers['x-api-version'] || '2';
  req.apiVersion = parseInt(version);

  // Warn about missing version header
  if (!req.headers['x-api-version']) {
    res.setHeader('Warning', '299 - "Using default version 2. Specify X-API-Version header."');
  }

  // Echo version in response
  res.setHeader('X-API-Version', version);
  next();
});

// Route handler with version-aware logic
app.get('/customers/:id', async (req, res) => {
  const customer = await db.customers.findById(req.params.id);

  if (req.apiVersion === 1) {
    // V1 response format (legacy)
    res.json({
      customerId: customer.id,
      customerName: customer.name
    });
  } else {
    // V2 response format (current)
    res.json({
      id: customer.id,
      name: customer.name,
      email: customer.email,
      createdAt: customer.createdAt
    });
  }
});

Standards & RFCs

Standards & RFCs
3)- [OpenAPI 3](https://reference.apios.info/terms/openapi-3/).x - API versioning header documentation