TL;DR — Start with URL path versioning for clarity and caching, use Header/media-type versioning when you need negotiation and fewer visible paths, and keep Schema evolution rules to avoid breakage. Publish a deprecation policy, emit Sunset headers, route versions via your API gateway, and roll out changes with canary + traffic splitting.
As an Amazon Associate I earn from qualifying purchases.
Table of Contents
- Why API Versioning Still Matters in 2025
- Three Approaches: URL vs Header vs Schema
- Compatibility Rules That Prevent Fire Drills
- Caching, Gateways, and Routing
- Rollout & Deprecation Playbook
- Code & Config Snippets
- Checklist Before You Ship
- FAQ
Why API Versioning Still Matters in 2025
- APIs outlive teams. Without a strategy, tiny changes become breaking landmines.
- Cloud edges cache aggressively. Good versioning works with caches, not against them.
- Compliance & SLAs need predictable lifecycles and deprecation windows.
Three Approaches: URL vs Header vs Schema
1) URL Path Versioning (easiest, cache-friendly)
- Example:
/v1/orders,/v2/orders - Pros: Clear for clients, trivial to route in gateways/load balancers, plays well with CDNs.
- Cons: Path sprawl; version is “global” even if only one resource changes.
- Use when: Public APIs, strong caching, many third-party consumers.
2) Header / Media Type Versioning (negotiation-friendly)
- Example:
Accept: application/vnd.yourco.orders+json;version=2
orAccept: application/vnd.yourco.v2+json - Pros: Avoids path explosion; lets you evolve per-resource.
- Cons: Some proxies/tools don’t surface header differences in analytics; caching needs
Vary: Accept. - Use when: You need finer control and want to keep URLs stable.
3) Schema-First Evolution (contract + rules)
- OpenAPI/JSON Schema/Protobuf define the contract; you evolve it under rules:
- Additive changes (safe): add optional fields/endpoints.
- Breaking changes: remove/rename/required→optional changes.
- Pros: Language-agnostic; unlocks codegen and validation.
- Cons: Requires discipline and linting in CI.
Recommendation: Start with URL path for simplicity. Layer header negotiation for advanced clients. Keep a schema evolution policy in your repo to avoid accidental breaks.
Compatibility Rules That Prevent Fire Drills
- Backward compatible by default:
- Only add optional fields; never remove existing ones without a new version.
- Treat unknown fields as ignore-but-preserve on the server if possible.
- Forward compatibility:
- Clients ignore unknown fields.
- Use feature flags or expansion parameters for gradual exposure.
- Version numbers:
- Prefer major versions (v1, v2). Minor/patch are implementation details; don’t expose every patch in the URL.
- Consider calendar versioning only if your org is process-heavy and aligned on dates.
Caching, Gateways, and Routing
- CDN & Edge caching
- URL versioning: works out of the box; minimal
Varyheaders. - Header versioning: set
Vary: Accept(or custom header) or use cache key customization; otherwise responses will be mixed.
- URL versioning: works out of the box; minimal
- API gateway routing
- Route by path prefix
/v2/…or by headerAccept: …;version=2. - Mirror traffic for read-only endpoints to measure adoption.
- Route by path prefix
- Rate limiting & analytics
- Keep per-version counters (RPS, error rate, latency).
- Alert on usage of deprecated versions.
Rollout & Deprecation Playbook
T-90 days: Public announcement (blog, docs, status page). Provide migration examples and SDK notes.
T-60 days: Add Deprecation response header and Link to docs.
T-30 days: Add Sunset header with RFC-compliant date; start targeted emails/dashboards to heavy consumers.
T-7 days: Throttle and A/B route a small slice to the new version in non-critical regions.
T day: Switch default; keep old version accessible behind an allow-list for a limited grace period.
Post: Publish a postmortem-lite with adoption metrics and learnings.
Code & Config Snippets
Express (Node.js) — URL vs Header detection
app.use((req, res, next) => {
const pathMatch = req.path.match(/^\/v(\d+)\//);
const urlVersion = pathMatch ? parseInt(pathMatch[1], 10) : null;
const accept = req.get('accept') || '';
const headerVersion = (() => {
const m = accept.match(/version=(\d+)/i) || accept.match(/vnd\.yourco\.v(\d+)/i);
return m ? parseInt(m[1], 10) : null;
})();
req.apiVersion = urlVersion || headerVersion || 1;
res.set('X-API-Version', String(req.apiVersion));
next();
});
NGINX — route by path or header
map $http_accept $api_version {
default 1;
"~*version=2" 2;
"~*vnd\.yourco\.v2\+json" 2;
}
server {
location ~ ^/v2/ { proxy_pass http://api_v2; }
location ~ ^/v1/ { proxy_pass http://api_v1; }
# Optional header-based route
location /orders {
if ($api_version = 2) { proxy_pass http://api_v2; break; }
proxy_pass http://api_v1;
}
}
OpenAPI — marking deprecations & examples
openapi: 3.0.3
info:
title: YourCo Orders API
version: "2.0.0"
paths:
/v2/orders:
get:
summary: List orders
deprecated: false
/v1/orders:
get:
summary: List orders (deprecated)
deprecated: true
responses:
'299':
description: "Deprecated: see Link header for migration guide"
JSON Schema — additive changes only
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Order",
"type": "object",
"properties": {
"id": { "type": "string" },
"total": { "type": "number" },
"notes": { "type": "string" } // added in v2, optional
},
"required": ["id", "total"],
"additionalProperties": true
}
Protobuf (gRPC) — versioned package
syntax = "proto3";
package yourco.orders.v2; // bump on breaking changes
message Order {
string id = 1;
double total = 2;
string notes = 3; // added in v2, optional
}
service Orders {
rpc List (ListRequest) returns (ListResponse);
}
Checklist Before You Ship
- Chosen default method (URL path) and documented header negotiation.
- Published schema evolution rules (additive-only unless bumping major).
- OpenAPI/Protobuf updated with
deprecatedmarkers and examples. - Gateway routes per version; analytics track per-version usage.
- Deprecation timeline drafted (announce → Deprecation → Sunset → default switch).
- Canary & traffic splitting plan with rollback triggers (error rate/latency).
- Caching set correctly (
Vary: Acceptif using header method). - Status page & docs updated; migration examples verified.
FAQ
Q: URL vs Header—what should I default to?
A: URL. It’s simple and cache-friendly. Add header negotiation as an advanced option.
Q: How many versions should be live at once?
A: Keep at most two major versions in production: current and previous.
Q: Do I need SemVer in the URL?
A: Not necessary. Use v1, v2 and maintain SemVer in your schema artifacts and SDKs.
Q: How do I warn clients early?
A: Deprecation headers (Deprecation, Sunset), a status page entry, and direct notifications for heavy consumers.
CTA (generic, no brand names)
- Try a managed API gateway free tier to route by path or header and collect per-version metrics.
- Grab the deprecation timeline checklist (PDF) and paste it into your internal runbook.
- Download the example configs (NGINX, Express, OpenAPI, JSON Schema, Protobuf) as a starter kit.