Skip to content

API Versioning Strategies — Best Practices and Implementation Guide

DodaTech Updated 2026-06-23 7 min read

API Versioning is the practice of managing changes to an API over time by assigning version identifiers, enabling backward compatibility so existing clients continue working while new features are released.

What You'll Learn

You will learn four major API Versioning strategies URI versioning, header versioning, query parameter versioning, and content negotiation along with their trade-offs and implementation patterns.

Why API Versioning Matters

Without versioning, a single breaking change can crash every client using your API. Mobile apps users cannot update immediately, third-party integrations may not be maintained, and internal services run on different release cycles. Proper versioning protects clients from breaking changes and gives you freedom to evolve your API.

Real-World Use

DodaTech operates multiple API versions simultaneously. Doda Browser v1 uses URI versioning, DodaZIP update service uses header versioning for fine-grained control, and Durga Antivirus Pro threat reporting uses content negotiation to support both JSON and Protocol Buffers formats.

API Versioning Learning Path

flowchart LR
  A[REST API Design] --> B[Why Version?]
  B --> C[URI Versioning]
  B --> D[Header Versioning]
  B --> E[Query Parameter]
  B --> F[Content Negotiation]
  C --> G[Choose Strategy]
  D --> G
  E --> G
  F --> G
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

Prerequisites

Understand RESTful Api Design Best Practices and HTTP Protocol Basics. Familiarity with API Development Concepts helps. You should know how to make HTTP requests with different headers and understand the concept of breaking vs non-breaking changes.

When to Version Your API

Not every change requires a new version. Classify changes as breaking or non-breaking.

Non-Breaking Changes

These can be released without versioning:

  • Adding a new endpoint
  • Adding optional fields to a response
  • Adding new query parameters
  • Relaxing validation rules
  • Increasing rate limits
  • Adding new HTTP methods to existing resources

Breaking Changes

These require a new version:

  • Removing an endpoint
  • Removing or renaming fields
  • Changing field types
  • Making optional fields required
  • Changing endpoint URLs
  • Changing authentication requirements
  • Changing error response format

Strategy 1: URI Versioning

The most common approach. The version is part of the URL path.

GET /api/v1/users
GET /api/v2/users

Implementation in Express.js

const express = require("express");
const app = express();

// v1 routes
app.get("/api/v1/users", (req, res) => {
  res.json({
    version: "1.0",
    data: [
      { id: 1, name: "Alice", email: "alice"@example".com" }
    ]
  });
});

// v2 routes (added role field)
app.get("/api/v2/users", (req, res) => {
  res.json({
    version: "2.0",
    data: [
      { id: 1, name: "Alice", email: "alice"@example".com", role: "admin" }
    ]
  });
});

Implementation in FastAPI

from fastapi import FastAPI, APIRouter

v1 = APIRouter(prefix="/api/v1")
v2 = APIRouter(prefix="/api/v2")

@v1.get("/users")
async def get_users_v1():
    return {"version": "1.0", "data": [
        {"id": 1, "name": "Alice"}
    ]}

@v2.get("/users")
async def get_users_v2():
    return {"version": "2.0", "data": [
        {"id": 1, "name": "Alice", "role": "admin"}
    ]}

app = FastAPI()
app.include_router(v1)
app.include_router(v2)

Pros: Simple, explicit, cache-friendly, easy to route. Cons: URL pollution, violates REST principle of a consistent URI for a resource.

Strategy 2: Header Versioning

The version is specified in a custom HTTP header.

GET /api/users
Accept: application/vnd.dodatech.v1+json

Implementation in Express.js

app.get("/api/users", (req, res) => {
  const accept = req.headers["accept"];

  if (accept.includes("vnd.dodatech.v1+json")) {
    return res.json({ version: "1.0", data: usersV1 });
  }

  if (accept.includes("vnd.dodatech.v2+json")) {
    return res.json({ version: "2.0", data: usersV2 });
  }

  // Default to latest version
  return res.json({ version: "2.0", data: usersV2 });
});

Pros: Clean URLs, REST-friendly, supports content negotiation. Cons: Harder to test in browsers, requires header modification, LESS visible to caching layers.

Strategy 3: Query Parameter Versioning

The version is a query parameter.

GET /api/users?version=1
GET /api/users?version=2

Implementation in Express.js

app.get("/api/users", (req, res) => {
  const version = req.query.version || "1";

  if (version === "1") {
    return res.json({ version: "1.0", data: usersV1 });
  }

  if (version === "2") {
    return res.json({ version: "2.0", data: usersV2 });
  }

  return res.status(400).json({ error: "Unsupported version" });
});

Pros: Easy to test, simple to implement. Cons: Can be forgotten by clients, pollutes query namespace, not RESTful.

Strategy 4: Content Negotiation

Use the standard Accept header with custom media types.

GET /api/users
Accept: application/json; version=1

Implementation in Express.js

app.get("/API/users", (req, res) => {
  const accept = req.headers["accept"] || "";

  if (accept.includes("version=1")) {
    return res.JSON({ version: "1.0", data: usersV1 });
  }

  if (accept.includes("version=2")) {
    return res.JSON({ version: "2.0", data: usersV2 });
  }

  return res.JSON({ version: "2.0", data: usersV2 });
});

Pros: Uses standard HTTP mechanism, clean URLs, supports multiple formats. Cons: Complex to implement, LESS discoverable.

Version Lifecycle Management

Define a clear lifecycle for each version:

const VERSIONS = {
  v1: { status: "deprecated", sunset: "2026-12-31" },
  v2: { status: "active", sunset: null },
  v3: { status: "beta", sunset: null }
};

app.get("/API/v1/users", (req, res) => {
  res.set("Sunset", VERSIONS.v1.sunset);
  res.set("Deprecation", "true");
  res.JSON({ version: "1.0", data: usersV1 });
});

Send deprecation headers to inform clients:

  • Deprecation: true — indicates the version is deprecated
  • Sunset: Sat, 31 Dec 2026 23:59:59 GMT — when the version will be removed
  • Link: </api/v2/users>; rel="successor-version" — points to the replacement

Common Errors

  1. Not versioning from the start — Releasing an API without versioning, then needing to add it later. All early clients break when you introduce versions. Always include versioning from the first release, even if only v1 exists.

  2. Removing old versions too quickly — Deprecating and removing versions before clients can migrate. Give at least six months notice before removing a version. Use the Sunset header to communicate timelines.

  3. Versioning the entire API instead of specific resources — A single change to one endpoint forces a full API version bump. Consider per-resource versioning for large APIs.

  4. Using semantic versioning for APIs — Versions like v1.2.3 imply frequent changes. Use simple integer versions (v1, v2, v3) for APIs. Save semantic versioning for the API implementation, not the API contract.

  5. No deprecation headers — Removing versions without warning clients. Always send Deprecation and Sunset headers on deprecated versions.

  6. Supporting too many versions — Maintaining five or more API versions simultaneously. Support a maximum of three versions current, previous, and beta.

  7. Inconsistent versioning across endpoints — Some endpoints on v1, others on v2. All endpoints in a version should be available and consistent. Version the entire API surface together.

Practice Questions

  1. What types of changes require a new API version?
  2. What are the advantages of URI versioning over header versioning?
  3. How should you communicate deprecation to API clients?
  4. What is the recommended number of API versions to support simultaneously?
  5. How does content negotiation differ from custom header versioning?

Challenge

Design an API Versioning Strategy for a SaaS platform with 50,000 active API clients. The platform currently has v1 with 200 endpoints. You need to introduce v2 that changes 30 endpoints. Write a Migration plan that includes: the versioning Strategy you choose and why, deprecation header implementation, sunset timeline (12 months), client communication plan, and surge pricing for v1 after the sunset date.

FAQ

Should I always include versioning even for internal APIs? Yes. Internal APIs change just as frequently as external ones. Without versioning, updating an internal API breaks every consuming service. Versioning protects internal consumers and gives you deployment flexibility.

What happens if I never version my API? Eventually you will make a breaking change. Without versioning, clients break immediately and cannot use the previous behavior. You end up maintaining undocumented backward-compatibility hacks that become Technical Debt.

How do I test multiple API versions? Run version-specific test suites against each deployed version. Use separate test collections in Postman for each version. Maintain integration tests that run against all active versions before deployment.

Can I use API gateways for versioning? Yes. API gateways like Kong, NGINX, and AWS API Gateway can route requests to different backend services based on version. This allows running different versions on different infrastructure.

Should I include the version in the response body? Include the version in response headers (X-API-Version: 2) rather than the body. The body should represent the resource. Use headers for metadata about the response.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro