Skip to content

Semantic Versioning for APIs — Understanding SemVer in Practice

DodaTech Updated 2026-06-28 5 min read

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

  1. What does a MAJOR version bump indicate?
  2. When should you use pre-release versioning?
  3. How does SemVer help API consumers?
  4. What is the difference between MINOR and PATCH changes?
  5. 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

What is SemVer for APIs?

SemVer (Semantic Versioning) uses MAJOR.MINOR.PATCH format where MAJOR changes break compatibility, MINOR changes add functionality backward-compatibly, and PATCH changes fix bugs backward-compatibly.

When should I reach 1.0.0?

Release 1.0.0 when your API is stable and you are making a compatibility guarantee to consumers. Before 1.0.0, anything can change.

Can I skip major versions?

You can skip version numbers but each MAJOR version should represent a distinct set of features with documented breaking changes from the previous version.

What is the difference between SemVer and API versioning?

SemVer is a versioning scheme. API versioning is the overall strategy for managing API changes. You can use SemVer within your API versioning strategy.

How do pre-release versions work?

Pre-release versions (1.0.0-beta.1) indicate the version is not stable. They have lower precedence than the normal version and signal that breaking changes may still occur.

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