API Versioning Basics — Why, When, and How to Version Your API
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
- Why is API versioning necessary?
- What is the difference between backward-compatible and breaking changes?
- How long should you support a deprecated API version?
- What is a sunset policy and why is it important?
- 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
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