API Versioning & Backward Compatibility
Design API versioning strategies that evolve gracefully without breaking existing consumers.
Activation Triggers
Activate on: "API versioning", "backward compatibility", "deprecation", "breaking change", "API migration", "v1 v2", "sunset header", "API evolution", "non-breaking change"
NOT for: Data schema evolution → schema-evolution-manager | Gateway version routing → api-gateway-reverse-proxy-expert | GraphQL deprecation → graphql-server-architect
Quick Start
- Classify the change — additive (safe), modification (maybe breaking), removal (breaking)
- Choose strategy — URL path (
/v2/), header (API-Version), or content negotiation
- Implement Sunset headers — RFC 8594 tells consumers when old versions die
- Run versions in parallel — minimum 6-month overlap for major versions
- Monitor adoption — track per-version traffic to know when to retire
Core Capabilities
| Domain |
Technologies |
| URL Versioning |
/api/v1/, /api/v2/ path-based routing |
| Header Versioning |
API-Version: 2024-01-15, Accept-Version |
| Content Negotiation |
Accept: application/vnd.myapi.v2+json |
| Deprecation |
Sunset header (RFC 8594), Deprecation header |
| Tooling |
OpenAPI 3.1 overlays, Optic, Bump.sh |
Architecture Patterns
Versioning Strategy Decision Tree
Is it additive only? (new fields, new endpoints)
├─ YES → No version bump needed (backward compatible)
└─ NO → Is it a field rename/type change?
├─ YES → Can you keep both old + new fields?
│ ├─ YES → Add new, deprecate old (minor version)
│ └─ NO → Major version bump (v1 → v2)
└─ NO → Is it a removal?
└─ YES → Major version bump with sunset period
Parallel Version Deployment
// Express router with version-based routing
import { Router } from 'express';
const v1Router = Router();
const v2Router = Router();
// v1: returns { name: string }
v1Router.get('/users/:id', async (req, res) => {
const user = await getUser(req.params.id);
res.json({ name: user.fullName }); // legacy shape
});
// v2: returns { firstName, lastName, displayName }
v2Router.get('/users/:id', async (req, res) => {
const user = await getUser(req.params.id);
res.json({
firstName: user.firstName,
lastName: user.lastName,
displayName: user.fullName,
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Sunset header middleware for v1
v1Router.use((req, res, next) => {
res.set('Sunset', 'Sat, 01 Nov 2026 00:00:00 GMT');
res.set('Deprecation', 'true');
res.set('Link', '</api/v2>; rel="successor-version"');
next();
});
Date-Based Versioning (Stripe Model)
API-Version: 2026-03-15
Changes are tied to dates, not integers:
2026-01-01 → baseline
2026-03-15 → renamed `name` → `display_name`
2026-06-01 → removed `legacy_field`
Server pins unversioned requests to the account's default version.
Each version is a transform layer over the canonical internal model.
Anti-Patterns
- Versioning too eagerly — most changes are additive and do not require a version bump; add fields freely
- No sunset timeline — deprecating without a concrete shutdown date means versions live forever
- Copying entire controllers — use a transform/adapter layer, not duplicated business logic per version
- Ignoring unknown fields — APIs should be tolerant readers; ignore unknown fields on input (Postel's Law)
- Breaking changes in minor versions — if consumers relied on it, removing it is breaking regardless of your intent
Quality Checklist
1---2name: api-versioning-backward-compatibility3description: API migration strategies, deprecation workflows, and header/URL/content versioning. Activate on: API versioning, backward compatibility, deprecation, breaking change, API migration, v1 v2, sunset header. NOT for: schema evolution in data (use schema-evolution-manager), gateway routing (use api-gateway-reverse-proxy-expert).4license: Apache-2.05---67# API Versioning & Backward Compatibility89Design API versioning strategies that evolve gracefully without breaking existing consumers.1011## Activation Triggers1213**Activate on:** "API versioning", "backward compatibility", "deprecation", "breaking change", "API migration", "v1 v2", "sunset header", "API evolution", "non-breaking change"1415**NOT for:** Data schema evolution → `schema-evolution-manager` | Gateway version routing → `api-gateway-reverse-proxy-expert` | GraphQL deprecation → `graphql-server-architect`1617## Quick Start18191. **Classify the change** — additive (safe), modification (maybe breaking), removal (breaking)202. **Choose strategy** — URL path (`/v2/`), header (`API-Version`), or content negotiation213. **Implement Sunset headers** — RFC 8594 tells consumers when old versions die224. **Run versions in parallel** — minimum 6-month overlap for major versions235. **Monitor adoption** — track per-version traffic to know when to retire2425## Core Capabilities2627| Domain | Technologies |28|--------|-------------|29| **URL Versioning** | `/api/v1/`, `/api/v2/` path-based routing |30| **Header Versioning** | `API-Version: 2024-01-15`, `Accept-Version` |31| **Content Negotiation** | `Accept: application/vnd.myapi.v2+json` |32| **Deprecation** | Sunset header (RFC 8594), Deprecation header |33| **Tooling** | OpenAPI 3.1 overlays, Optic, Bump.sh |3435## Architecture Patterns3637### Versioning Strategy Decision Tree3839```40Is it additive only? (new fields, new endpoints)41 ├─ YES → No version bump needed (backward compatible)42 └─ NO → Is it a field rename/type change?43 ├─ YES → Can you keep both old + new fields?44 │ ├─ YES → Add new, deprecate old (minor version)45 │ └─ NO → Major version bump (v1 → v2)46 └─ NO → Is it a removal?47 └─ YES → Major version bump with sunset period48```4950### Parallel Version Deployment5152```typescript53// Express router with version-based routing54import { Router } from 'express';5556const v1Router = Router();57const v2Router = Router();5859// v1: returns { name: string }60v1Router.get('/users/:id', async (req, res) => {61 const user = await getUser(req.params.id);62 res.json({ name: user.fullName }); // legacy shape63});6465// v2: returns { firstName, lastName, displayName }66v2Router.get('/users/:id', async (req, res) => {67 const user = await getUser(req.params.id);68 res.json({69 firstName: user.firstName,70 lastName: user.lastName,71 displayName: user.fullName,72 });73});7475app.use('/api/v1', v1Router);76app.use('/api/v2', v2Router);7778// Sunset header middleware for v179v1Router.use((req, res, next) => {80 res.set('Sunset', 'Sat, 01 Nov 2026 00:00:00 GMT');81 res.set('Deprecation', 'true');82 res.set('Link', '</api/v2>; rel="successor-version"');83 next();84});85```8687### Date-Based Versioning (Stripe Model)8889```90API-Version: 2026-03-159192Changes are tied to dates, not integers:93 2026-01-01 → baseline94 2026-03-15 → renamed `name` → `display_name`95 2026-06-01 → removed `legacy_field`9697Server pins unversioned requests to the account's default version.98Each version is a transform layer over the canonical internal model.99```100101## Anti-Patterns1021031. **Versioning too eagerly** — most changes are additive and do not require a version bump; add fields freely1042. **No sunset timeline** — deprecating without a concrete shutdown date means versions live forever1053. **Copying entire controllers** — use a transform/adapter layer, not duplicated business logic per version1064. **Ignoring unknown fields** — APIs should be tolerant readers; ignore unknown fields on input (Postel's Law)1075. **Breaking changes in minor versions** — if consumers relied on it, removing it is breaking regardless of your intent108109## Quality Checklist110111- [ ] Additive changes (new fields, new endpoints) ship without version bump112- [ ] Breaking changes require major version with 6+ month sunset period113- [ ] Sunset header (RFC 8594) set on deprecated versions114- [ ] Link header points consumers to successor version115- [ ] Per-version traffic monitored to track migration progress116- [ ] OpenAPI spec published per version with diff tooling (Optic/Bump.sh)117- [ ] Consumer notification plan: changelog, email, dashboard warning118- [ ] Version routing handled at gateway level, not in application code119- [ ] Integration tests validate backward compatibility (contract tests)