Semantic Versioning for APIs — Understanding SemVer in Practice
In this tutorial, you'll learn about Versioning Semver. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Semantic versioning for APIs uses a MAJOR.MINOR.PATCH format to communicate the nature of changes and their compatibility impact to API consumers.
What You'll Learn
By the end of this lesson, you will apply SemVer to API versions, identify breaking vs non-breaking changes, use pre-release versioning, and communicate compatibility guarantees through version numbers.
Why It Matters
SemVer provides a universal language for communicating API change severity. Consumers can confidently upgrade based on version numbers without reading every changelog entry.
Real-World Use
Durga Antivirus Pro versions its Threat Intelligence API as v2.5.1, where MAJOR 2 indicates the feature set, MINOR 5 indicates backward-compatible additions, and PATCH 1 indicates bug fixes.
SemVer Structure
flowchart LR
Version[2.5.1]-->Major[MAJOR - Breaking Changes]
Version-->Minor[MINOR - New Features]
Version-->Patch[PATCH - Bug Fixes]
Major-->Incompatible[Not backward compatible]
Minor-->Compatible[Backward compatible additions]
Patch-->Fixes[Backward compatible fixes]
SemVer Parser
A parser that extracts and interprets SemVer components.
import re
from typing import Optional, Tuple, Dict
class SemVer:
def __init__(self, version_string: str):
self.raw = version_string
self.major = 0
self.minor = 0
self.patch = 0
self.pre_release: Optional[str] = None
self.build: Optional[str] = None
self._parse()
def _parse(self):
pattern = (
r"^(\d+)\.(\d+)\.(\d+)"
r"(?:-([0-9A-Za-z.-]+))?"
r"(?:\+([0-9A-Za-z.-]+))?$"
)
match = re.match(pattern, self.raw)
if not match:
raise ValueError(
f"Invalid SemVer: {self.raw}"
)
self.major = int(match.group(1))
self.minor = int(match.group(2))
self.patch = int(match.group(3))
self.pre_release = match.group(4)
self.build = match.group(5)
def is_compatible_with(self, other: "SemVer") -> bool:
return self.major == other.major
def is_stable(self) -> bool:
return self.major >= 1 and not self.pre_release
def bump_major(self) -> "SemVer":
return SemVer(
f"{self.major + 1}.0.0"
)
def bump_minor(self) -> "SemVer":
return SemVer(
f"{self.major}.{self.minor + 1}.0"
)
def bump_patch(self) -> "SemVer":
return SemVer(
f"{self.major}.{self.minor}.{self.patch + 1}"
)
def compare(self, other: "SemVer") -> int:
for attr in ["major", "minor", "patch"]:
diff = getattr(self, attr) - getattr(other, attr)
if diff != 0:
return -1 if diff < 0 else 1
return 0
def __str__(self) -> str:
return self.raw
v1 = SemVer("2.5.1")
v2 = SemVer("2.6.0")
print(f"{v1} compatible with {v2}: {v1.is_compatible_with(v2)}")
print(f"{v1} stable: {v1.is_stable()}")
print(f"Bump major: {v1.bump_major()}")
print(f"Compare: {v1.compare(v2)}")
Breaking Change Detection
Identify whether a change is breaking based on SemVer rules.
from typing import Dict, List, Optional, Tuple
from enum import Enum
class ChangeType(Enum):
BREAKING = "breaking"
NON_BREAKING = "non_breaking"
PATCH = "patch"
class BreakingChangeDetector:
def __init__(self):
self.breaking_patterns = [
"remove_endpoint",
"remove_field",
"change_field_type",
"make_field_required",
"change_auth_requirement",
"change_response_format",
]
def classify_change(self, change: Dict) -> ChangeType:
change_type = change.get("type", "")
if change_type in self.breaking_patterns:
return ChangeType.BREAKING
if change_type in ["add_endpoint", "add_field"]:
return ChangeType.NON_BREAKING
return ChangeType.PATCH
def determine_next_version(
self, current_version: SemVer,
changes: List[Dict]
) -> SemVer:
has_breaking = False
has_feature = False
for change in changes:
ctype = self.classify_change(change)
if ctype == ChangeType.BREAKING:
has_breaking = True
elif ctype == ChangeType.NON_BREAKING:
has_feature = True
if has_breaking:
return current_version.bump_major()
if has_feature:
return current_version.bump_minor()
return current_version.bump_patch()
detector = BreakingChangeDetector()
current = SemVer("1.0.0")
changes = [
{"type": "add_endpoint",
"description": "New scan endpoint"},
{"type": "remove_field",
"description": "Remove deprecated field"},
]
next_ver = detector.determine_next_version(current, changes)
print(f"Current: {current}, Next: {next_ver}")
Version Compatibility Matrix
Track which client versions are compatible with which API versions.
from typing import Dict, List, Optional, Tuple
class CompatibilityMatrix:
def __init__(self):
self.matrix: Dict[str, List[str]] = {}
def add_compatibility(self, api_version: str,
min_client_version: str):
if api_version not in self.matrix:
self.matrix[api_version] = []
self.matrix[api_version].append(min_client_version)
def is_compatible(self, api_version: str,
client_version: str) -> bool:
allowed = self.matrix.get(api_version, [])
if not allowed:
return True
return client_version in allowed
def get_compatible_clients(
self, api_version: str
) -> List[str]:
return self.matrix.get(api_version, [])
def recommend_upgrade(
self, current_api: str,
target_api: str
) -> Dict:
return {
"current": current_api,
"target": target_api,
"breaking_changes": [
"Response format changed",
"Auth requirement added"
]
}
matrix = CompatibilityMatrix()
matrix.add_compatibility("2.0.0", "1.5.0")
matrix.add_compatibility("2.0.0", "1.6.0")
compat = matrix.is_compatible("2.0.0", "1.4.0")
print(f"Client 1.4.0 compatible with API 2.0.0: {compat}")
Common Mistakes
Mistake 1: Breaking Changes Without Major Version Bump
Adding a required field to a response is breaking. Bump MAJOR, not MINOR.
Mistake 2: Using Version Numbers as Release Dates
0.x versions are fine for development. Once stable at 1.0.0, respect the SemVer contract.
Mistake 3: Ignoring Pre-release Tags
Pre-release versions (1.0.0-alpha.1) indicate instability. Consumers should opt in explicitly.
Mistake 4: Not Communicating Deprecation
Mark fields and endpoints as deprecated before removal. Give consumers at least one release cycle to migrate.
Mistake 5: Mixing SemVer with Other Schemes
Do not use date-based versions alongside SemVer. Pick one scheme and stick to it.
Practice Questions
- What does a MAJOR version bump indicate?
- When should you use pre-release versioning?
- How does SemVer help API consumers?
- What is the difference between MINOR and PATCH changes?
- How do you handle breaking changes in a 0.x version?
Challenge
Build a SemVer-based API Versioning system that parses version strings, classifies changes as breaking/non-breaking/patch, automatically determines the next version, and outputs a compatibility report for consumers.
FAQ
Mini Project
Build a SemVer API versioning tool that parses SemVer strings, compares versions, classifies changes by type, auto-increments the correct segment, validates version strings, and outputs recommended next version for a set of changes.
What's Next
Learn about Breaking Changes and how to manage them, or explore Versioning Basics for fundamental API versioning concepts.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro