Ai api-versioning
API versioning strategies — URL path, header, query param, content negotiation — with breaking change classification, deprecation timelines, migration patterns, and multi-version support. Use when evolving APIs, planning breaking changes, or managing version lifecycles.
git clone https://github.com/wpank/ai
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/api/api-versioning" ~/.claude/skills/wpank-ai-api-versioning && rm -rf "$T"
skills/api/api-versioning/SKILL.mdAPI Versioning Patterns
Evolve your API confidently. Version correctly, deprecate gracefully, migrate safely — without breaking existing consumers.
Versioning Strategies
Pick one strategy and apply it consistently across your entire API surface.
| Strategy | Format | Visibility | Cacheability | Best For |
|---|---|---|---|---|
| URL Path | | High | Excellent | Public APIs, third-party integrations |
| Query Param | | Medium | Moderate | Simple APIs, prototyping |
| Header | | Low | Good | Internal APIs, coordinated consumers |
| Content Negotiation | | Low | Good | Enterprise, strict REST compliance |
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install api-versioning
URL Path Versioning
The most common strategy. Version lives in the URL, making it immediately visible.
from fastapi import FastAPI, APIRouter v1 = APIRouter(prefix="/api/v1") v2 = APIRouter(prefix="/api/v2") @v1.get("/users") async def list_users_v1(): return {"users": [...]} @v2.get("/users") async def list_users_v2(): return {"data": {"users": [...]}, "meta": {...}} app = FastAPI() app.include_router(v1) app.include_router(v2)
Rules:
- Always prefix:
not/api/v1/.../v1/api/... - Major version only:
, never/api/v1/
or/api/v1.2//api/v1.2.3/ - Every endpoint must be versioned — no mixing versioned and unversioned paths
Header Versioning
Version specified via request headers, keeping URLs clean.
function versionRouter(req, res, next) { const version = req.headers['accept-version'] || 'v2'; // default to latest req.apiVersion = version; next(); } app.get('/api/users', versionRouter, (req, res) => { if (req.apiVersion === 'v1') return res.json({ users: [...] }); if (req.apiVersion === 'v2') return res.json({ data: { users: [...] }, meta: {} }); return res.status(400).json({ error: `Unsupported version: ${req.apiVersion}` }); });
Always define fallback behavior when no version header is sent — default to latest stable or return
400 Bad Request.
Semantic Versioning for APIs
| SemVer Component | API Meaning | Action Required |
|---|---|---|
| MAJOR (v1 → v2) | Breaking changes — remove field, rename endpoint, change auth | Clients must migrate |
| MINOR (v1.1 → v1.2) | Additive, backward-compatible — new optional field, new endpoint | No client changes |
| PATCH (v1.1.0 → v1.1.1) | Bug fixes, no behavior change | No client changes |
Only MAJOR versions appear in URL paths. Communicate MINOR and PATCH through changelogs.
Breaking vs Non-Breaking Changes
Breaking — Require New Version
| Change | Why It Breaks |
|---|---|
| Remove a response field | Clients reading that field get |
| Rename a field | Same as removal from the client's perspective |
| Change a field's type | → breaks typed clients |
| Remove an endpoint | Clients calling it get |
| Make optional param required | Existing requests missing it start failing |
| Change URL structure | Bookmarked/hardcoded URLs break |
| Change error response format | Client error-handling logic breaks |
| Change authentication mechanism | Existing credentials stop working |
Non-Breaking — Safe Under Same Version
| Change | Why It's Safe |
|---|---|
| Add new optional response field | Clients ignore unknown fields |
| Add new endpoint | Doesn't affect existing endpoints |
| Add new optional query/body param | Existing requests work without it |
| Add new enum value | Safe if clients handle unknown values gracefully |
| Relax a validation constraint | Previously valid requests remain valid |
| Improve performance | Same interface, faster response |
Deprecation Strategy
Never remove a version without warning. Follow this timeline:
Phase 1: ANNOUNCE • Sunset header on responses • Changelog entry • Email/webhook to consumers • Docs marked "deprecated" Phase 2: SUNSET PERIOD • v1 still works but warns • Monitor v1 traffic • Contact remaining consumers • Provide migration support Phase 3: REMOVAL • v1 returns 410 Gone • Response body includes migration guide URL • Redirect docs to v2
Minimum deprecation periods: Public API: 12 months · Partner API: 6 months · Internal API: 1–3 months
Sunset HTTP Header (RFC 8594)
Include on every response from a deprecated version:
HTTP/1.1 200 OK Sunset: Sat, 01 Mar 2025 00:00:00 GMT Deprecation: true Link: <https://api.example.com/docs/migrate-v1-v2>; rel="sunset" X-API-Warn: "v1 is deprecated. Migrate to v2 by 2025-03-01."
Retired Version Response
When past sunset, return
410 Gone:
{ "error": "VersionRetired", "message": "API v1 was retired on 2025-03-01.", "migration_guide": "https://api.example.com/docs/migrate-v1-v2", "current_version": "v2" }
Migration Patterns
Adapter Pattern
Shared business logic, version-specific serialization:
class UserService: async def get_user(self, user_id: str) -> User: return await self.repo.find(user_id) def to_v1(user: User) -> dict: return {"id": user.id, "name": user.full_name, "email": user.email} def to_v2(user: User) -> dict: return { "id": user.id, "name": {"first": user.first_name, "last": user.last_name}, "emails": [{"address": e, "primary": i == 0} for i, e in enumerate(user.emails)], "created_at": user.created_at.isoformat(), }
Facade Pattern
Single entry point delegates to the correct versioned handler:
async def get_user(user_id: str, version: int): user = await user_service.get_user(user_id) serializers = {1: to_v1, 2: to_v2} serialize = serializers.get(version) if not serialize: raise UnsupportedVersionError(version) return serialize(user)
Versioned Controllers
Separate controller files per version, shared service layer:
api/ v1/ users.py # v1 request/response shapes orders.py v2/ users.py # v2 request/response shapes orders.py services/ user_service.py # version-agnostic business logic order_service.py
API Gateway Routing
Route versions at infrastructure layer:
routes: - match: /api/v1/* upstream: api-v1-service:8080 - match: /api/v2/* upstream: api-v2-service:8080
Multi-Version Support
Architecture:
Request → API Gateway → Version Router → v1 Handler → Shared Service Layer → DB → v2 Handler ↗
Principles:
- Business logic is version-agnostic. Services, repositories, and domain models are shared.
- Serialization is version-specific. Each version has its own request validators and response serializers.
- Transformations are explicit. A
transformer documents every field mapping.v1_to_v2 - Tests cover all active versions. Every supported version has its own integration test suite.
Maximum concurrent versions: 2–3 active (current + 1–2 deprecated). More than 3 creates unsustainable maintenance burden.
Client Communication
Changelog
Publish a changelog for every release, tagged by version and change type:
## v2.3.0 — 2025-02-01 ### Added - `avatar_url` field on User response - `GET /api/v2/users/{id}/activity` endpoint ### Deprecated - `name` field on User — use `first_name` and `last_name` (removal in v3)
Migration Guides
For every major version bump, provide:
- Field-by-field mapping table (v1 → v2)
- Before/after request and response examples
- Code snippets for common languages/SDKs
- Timeline with key dates (announcement, sunset, removal)
SDK Versioning
Align SDK major versions with API major versions:
api-client@1.x → /api/v1 api-client@2.x → /api/v2
Ship the new SDK before announcing API deprecation.
Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
| Versioning too frequently | Batch breaking changes into infrequent major releases |
| Breaking without notice | Always follow the deprecation timeline |
| Eternal version support | Set and enforce sunset dates |
| Inconsistent versioning | One version scheme, applied uniformly |
| Version per endpoint | Version the entire API surface together |
| Using versions to gate features | Use feature flags separately; versions are for contracts |
| No default version | Always define a default or return explicit 400 |
NEVER Do
- NEVER remove a field, endpoint, or change a type without bumping the major version
- NEVER sunset a public API version with less than 6 months notice
- NEVER mix versioning strategies in the same API (URL path for some, headers for others)
- NEVER use minor or patch versions in URL paths (
is wrong — use/api/v1.2/
)/api/v1/ - NEVER version individual endpoints independently — version the entire API surface as a unit
- NEVER deploy a breaking change under an existing version number, even if "nobody uses that field"
- NEVER skip documenting differences between versions — every breaking change needs a migration guide entry