API Versioning Made Clear: URL vs Header vs Schema

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

  1. Why API Versioning Still Matters in 2025
  2. Three Approaches: URL vs Header vs Schema
  3. Compatibility Rules That Prevent Fire Drills
  4. Caching, Gateways, and Routing
  5. Rollout & Deprecation Playbook
  6. Code & Config Snippets
  7. Checklist Before You Ship
  8. 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
    or Accept: 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 Vary headers.
    • Header versioning: set Vary: Accept (or custom header) or use cache key customization; otherwise responses will be mixed.
  • API gateway routing
    • Route by path prefix /v2/… or by header Accept: …;version=2.
    • Mirror traffic for read-only endpoints to measure adoption.
  • 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 deprecated markers 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: Accept if 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.

Leave a Reply

Your email address will not be published. Required fields are marked *