Skip to content

API Versioning Basics — Why, When, and How to Version Your API

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about Versioning Basics. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

API Versioning is the practice of managing changes to your API over time, allowing consumers to continue using older versions while new versions are introduced.

What You'll Learn

By the end of this lesson, you will understand why versioning is necessary, know when to introduce a new version, compare different versioning strategies, and choose the right approach for your API.

Why It Matters

Without versioning, any change to your API risks breaking existing consumers. Versioning enables continuous evolution without disrupting your user base.

Real-World Use

Durga Antivirus Pro maintains three active API versions (v1, v2, v3) simultaneously, giving consumers 12 months to migrate from v1 to v2 and 6 months from v2 to v3.

Versioning Timeline

flowchart LR
    V1[API v1]-->|Launch|Stable[Stable]
    V1-->|After 12 months|Deprecated[Deprecated]
    Deprecated-->|After 6 more months|Sunset[Sunset]
    V2[API v2]-->|Launch at month 6|Stable2[Stable]
    V2-->Deprecated2[Deprecated at month 18]
    V3[API v3]-->|Launch at month 12|Stable3[Stable]

Versioning Strategy Comparison

Compare different API versioning approaches.

from typing import Dict, List, Optional

class VersioningStrategy:
    def __init__(self, name: str,
                 description: str,
                 pros: List[str],
                 cons: List[str]):
        self.name = name
        self.description = description
        self.pros = pros
        self.cons = cons

    def summary(self) -> Dict:
        return {
            "name": self.name,
            "description": self.description,
            "pros": self.pros,
            "cons": self.cons,
        }

class StrategyGuide:
    def __init__(self):
        self.strategies: Dict[str, VersioningStrategy] = {}

    def add_strategy(self, strategy: VersioningStrategy):
        self.strategies[strategy.name] = strategy

    def compare(self, names: List[str]) -> List[Dict]:
        return [
            self.strategies[name].summary()
            for name in names if name in self.strategies
        ]

    def recommend(self, requirements: Dict) -> str:
        if requirements.get("public_api"):
            return "uri_versioning"
        if requirements.get("internal_api"):
            return "header_versioning"
        if requirements.get("graphql"):
            return "no_versioning"
        return "uri_versioning"

guide = StrategyGuide()
guide.add_strategy(VersioningStrategy(
    "uri_versioning",
    "Version in URL path: /api/v1/users",
    ["Visible", "Easy to route", "Cacheable"],
    ["URL pollution", "Hard to maintain multiple versions"]
))
guide.add_strategy(VersioningStrategy(
    "header_versioning",
    "Version in custom header: X-API-Version: 1",
    ["Clean URLs", "No URL pollution"],
    ["Less visible", "Harder to debug", "Not cacheable"]
))
guide.add_strategy(VersioningStrategy(
    "content_negotiation",
    "Version in Accept header: application/vnd.api.v1+json",
    ["Restful", "Content-based", "Clean"],
    ["Complex", "Hard for consumers", "Poor tooling"]
))
rec = guide.recommend({"public_api": True})
print(f"Recommended: {rec}")

Version Lifecycle

Define the lifecycle stages of an API version.

from datetime import datetime, timedelta
from typing import Dict, Optional, List
from enum import Enum

class VersionStage(Enum):
    DEVELOPMENT = "development"
    BETA = "beta"
    STABLE = "stable"
    DEPRECATED = "deprecated"
    SUNSET = "sunset"

class APIVersion:
    def __init__(self, version_id: str,
                 release_date: datetime,
                 stage: VersionStage = VersionStage.STABLE,
                 deprecation_date: Optional[datetime] = None,
                 sunset_date: Optional[datetime] = None):
        self.version_id = version_id
        self.release_date = release_date
        self.stage = stage
        self.deprecation_date = deprecation_date
        self.sunset_date = sunset_date

    def days_until_deprecation(self) -> Optional[int]:
        if self.deprecation_date:
            delta = self.deprecation_date - datetime.utcnow()
            return max(0, delta.days)
        return None

    def days_until_sunset(self) -> Optional[int]:
        if self.sunset_date:
            delta = self.sunset_date - datetime.utcnow()
            return max(0, delta.days)
        return None

    def is_active(self) -> bool:
        return self.stage in [
            VersionStage.STABLE,
            VersionStage.DEPRECATED
        ]

    def stage_summary(self) -> Dict:
        current_stage = self.stage.value
        if self.stage == VersionStage.DEPRECATED:
            days = self.days_until_sunset()
            return {
                "version": self.version_id,
                "stage": current_stage,
                "days_until_sunset": days,
                "action_required": days is not None and days < 90
            }
        return {
            "version": self.version_id,
            "stage": current_stage,
        }

v1 = APIVersion("v1", datetime(2024, 1, 1),
                VersionStage.DEPRECATED,
                datetime(2025, 1, 1),
                datetime(2026, 1, 1))
v2 = APIVersion("v2", datetime(2025, 6, 1),
                VersionStage.STABLE,
                datetime(2026, 6, 1),
                datetime(2027, 6, 1))
print(v1.stage_summary())
print(v2.stage_summary())

Migration Planning

Plan consumer migration between API versions.

from typing import Dict, List, Optional
from datetime import datetime

class MigrationPlan:
    def __init__(self, from_version: str,
                 to_version: str):
        self.from_version = from_version
        self.to_version = to_version
        self.breaking_changes: List[str] = []
        self.new_features: List[str] = []
        self.migration_steps: List[str] = []

    def add_breaking_change(self, change: str):
        self.breaking_changes.append(change)

    def add_new_feature(self, feature: str):
        self.new_features.append(feature)

    def add_step(self, step: str):
        self.migration_steps.append(step)

    def estimate_effort(self) -> str:
        changes = len(self.breaking_changes)
        if changes == 0:
            return "low"
        if changes <= 3:
            return "medium"
        return "high"

    def generate_guide(self) -> Dict:
        return {
            "from": self.from_version,
            "to": self.to_version,
            "effort": self.estimate_effort(),
            "breaking_changes": self.breaking_changes,
            "new_features": self.new_features,
            "migration_steps": self.migration_steps,
            "recommended_timeline": (
                "Complete migration within 3 months"
            ),
        }

plan = MigrationPlan("v1", "v2")
plan.add_breaking_change("Renamed 'scan_id' to 'id'")
plan.add_breaking_change("Removed deprecated 'status' field")
plan.add_breaking_change("Auth now requires Bearer token")
plan.add_step("Update API client library to v2")
plan.add_step("Update all endpoint URLs from /v1/ to /v2/")
plan.add_step("Update request/response field names")
plan.add_step("Run integration tests against v2")
print(f"Migration effort: {plan.estimate_effort()}")

Common Mistakes

Mistake 1: Not Versioning at All

APIs without versions lock all consumers to the current implementation, making evolution impossible.

Mistake 2: Versioning Too Often

Creating a new version for every small change creates consumer fatigue. Batch breaking changes into planned major releases.

Mistake 3: No Sunset Policy

Versions should have clear end-of-life dates. Without sunset dates, you support old versions indefinitely.

Mistake 4: Multiple Strategies Mixed

Using URL versioning for some endpoints and header versioning for others confuses consumers.

Mistake 5: Ignoring Internal Consumers

Internal services and frontend apps also need versioning consideration. Dont version for external but ignore internal.

Practice Questions

  1. Why is API versioning necessary?
  2. What is the difference between backward-compatible and breaking changes?
  3. How long should you support a deprecated API version?
  4. What is a sunset policy and why is it important?
  5. How do you choose between URL and header versioning?

Challenge

Design an API versioning strategy for a public API that supports three active versions, provides 12-month deprecation notice, includes clear migration guides between versions, and uses URL-based versioning for simplicity.

FAQ

Should I version my API from day one?

Yes. Even if you only have one version, plan for versioning from the start. It is much harder to add versioning later.

How many API versions should I support simultaneously?

Support 2-3 versions at most. More versions increase maintenance burden and confuse consumers.

What is the difference between versioning and revision?

Versioning is a major compatibility boundary. Revision is a minor bug fix within the same version, like a SemVer PATCH level.

Can I avoid versioning with backward compatibility?

You can minimize the need for versioning by maintaining backward compatibility, but eventually breaking changes are necessary for progress.

What happens when I sunset an API version?

The version stops working. Return 410 Gone with a Link header pointing to the new version. Traffic monitoring should confirm zero usage before sunset.

Mini Project

Build an API versioning strategy document that includes a version lifecycle policy (development, stable, deprecated, sunset stages), a comparison of URI and header versioning for your use case, a 12-month deprecation timeline, and a migration guide template.

What's Next

Learn about Semantic Versioning for version numbering, or explore URI Versioning Deep for URL-based versioning strategies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro