The most surprising thing about API versioning is that the "best" method often depends on who you’re trying to satisfy: the client or the server.

Let’s see versioning in action. Imagine we have two versions of a User API:

v1:

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: Get a list of users
      responses:
        '200':
          description: A list of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UserV1'
components:
  schemas:
    UserV1:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string

v2:

openapi: 3.0.0
info:
  title: User API
  version: 2.0.0
paths:
  /users:
    get:
      summary: Get a list of users
      responses:
        '200':
          description: A list of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UserV2'
components:
  schemas:
    UserV2:
      type: object
      properties:
        userId:
          type: string
        fullName:
          type: string
        contactEmail:
          type: string
        isActive:
          type: boolean

Notice v2 has renamed fields (id to userId, name to fullName, email to contactEmail) and added a new field (isActive).

Now, let’s explore how clients can request a specific version.

URI Versioning

This is the most straightforward approach for clients. The version is embedded directly in the URL.

Client Request for v1: GET /v1/users Client Request for v2: GET /v2/users

How it works: The server’s routing layer inspects the URI. If it sees /v1/, it knows to apply the v1 API definition and logic. If it sees /v2/, it uses the v2 definition.

Pros:

  • Client Simplicity: Easy for clients to understand and implement.
  • Clear Separation: Visually distinct versions in the URL.

Cons:

  • URI Pollution: Can lead to a large number of URIs if you have many versions or resources.
  • Server-Side Complexity: Might require more intricate routing configurations to handle multiple URI prefixes.

Example Server Config (Conceptual - e.g., Express.js):

app.get('/v1/users', (req, res) => { /* handle v1 users */ });
app.get('/v2/users', (req, res) => { /* handle v2 users */ });

Header Versioning

Here, the client sends the desired API version in a custom HTTP header. The most common header is Accept-Version.

Client Request for v1:

GET /users
Accept-Version: 1.0.0

Client Request for v2:

GET /users
Accept-Version: 2.0.0

How it works: The server-side API gateway or middleware intercepts the request, reads the Accept-Version header, and dispatches the request to the appropriate version handler based on the value.

Pros:

  • Clean URIs: Keeps resource paths consistent across versions.
  • Server-Side Control: More flexibility for the server to manage routing.

Cons:

  • Client Overhead: Clients need to explicitly add the header to every request.
  • Less Discoverable: Version information isn’t immediately visible in the URL.

Example Server Config (Conceptual):

app.get('/users', (req, res) => {
  const version = req.headers['accept-version'];
  if (version === '1.0.0') {
    // handle v1 users
  } else if (version === '2.0.0') {
    // handle v2 users
  } else {
    res.status(400).send('Unsupported version');
  }
});

Content-Type Versioning (Media Type Versioning)

This method uses the Content-Type or Accept header with a custom media type that includes the version.

Client Request for v1:

GET /users
Accept: application/vnd.myapp.v1+json

Client Request for v2:

GET /users
Accept: application/vnd.myapp.v2+json

How it works: The server examines the Accept header to determine which version of the response the client can handle. This is often preferred for responses, but can be used for requests too by including it in Content-Type if sending a payload. It aligns with HTTP’s content negotiation principles.

Pros:

  • HTTP Compliant: Leverages standard HTTP mechanisms for content negotiation.
  • Clear Intent: Explicitly states the desired content format.

Cons:

  • Complex Media Types: Can lead to verbose and potentially cumbersome Accept headers.
  • Client Implementation: Requires clients to correctly construct Accept headers, which can be tricky with libraries.

Example Server Config (Conceptual):

app.get('/users', (req, res) => {
  const acceptHeader = req.headers['accept'];
  if (acceptHeader && acceptHeader.includes('application/vnd.myapp.v1+json')) {
    // return v1 users
  } else if (acceptHeader && acceptHeader.includes('application/vnd.myapp.v2+json')) {
    // return v2 users
  } else {
    res.status(406).send('Not Acceptable'); // 406 Not Acceptable
  }
});

The Mental Model: Versioning as a Contract

Think of API versioning not just as different code paths, but as distinct contracts between the client and server. Each version represents a stable agreement on the data structure, endpoints, and behavior. When you version, you’re essentially saying, "Here’s a new contract. You can choose to adopt it, but the old one remains available for those who haven’t."

The real power, and complexity, lies in how the server mediates these contracts. Is it the router (URI), a middleware (Header), or the content negotiation engine (Content-Type) that enforces which version is served? Each method shifts the burden of "which contract" from one part of the system to another.

A subtle but crucial point is how each method affects caching. URI versioning is generally the most cache-friendly, as distinct URLs naturally lead to distinct cache entries. Header-based versioning can be more complex for caching layers, requiring them to inspect headers. Content-Type versioning, while HTTP-standard, can sometimes lead to duplicate cached resources if not handled carefully, especially if the base URI is the same.

The next hurdle you’ll face is managing deprecation strategies for older API versions.

Want structured learning?

Take the full Swagger course →