Skip to main content

REST API Versioning Strategies: URL, Header & Content Negotiation

API versioning lets you evolve your API without breaking existing clients. Each strategy has trade-offs between visibility, REST purity, and developer experience. This guide covers all four approaches with working examples and a pros/cons comparison.

✓ URL, header & content negotiation ✓ Breaking vs non-breaking changes ✓ Sunset & Deprecation headers ✓ Pros/cons comparison table

URL Path Versioning (Most Common)

The version is embedded in the URL path. Simple to understand, route, and document. Used by Stripe, GitHub, and most major public APIs:

# URL versioning — version in path prefix
GET https://api.example.com/v1/users/42
GET https://api.example.com/v2/users/42

# Express router setup — mount each version separately
import express from 'express';
import { v1Router } from './routes/v1';
import { v2Router } from './routes/v2';

const app = express();

app.use('/v1', v1Router);   // Legacy — maintained until 2027-01-01
app.use('/v2', v2Router);   // Current stable version

# Kong API Gateway routing by path prefix
services:
  - name: api-v1
    url: http://api-service:8080/v1
    routes:
      - paths: ['/v1']
  - name: api-v2
    url: http://api-service:8080/v2
    routes:
      - paths: ['/v2']

Header Versioning & Content Negotiation

Keep URLs clean by encoding the version in a request header or the Accept header. Both approaches require client-side discipline but produce cleaner resource URIs:

# Option A: Custom header versioning
GET /users/42
API-Version: 2
Authorization: Bearer eyJ...

# Option B: Content negotiation (most RESTful)
GET /users/42
Accept: application/vnd.example.v2+json
Authorization: Bearer eyJ...

# Express middleware — read version from header or Accept
function resolveVersion(req: Request, res: Response, next: NextFunction) {
  // Try custom header first
  let version = req.headers['api-version'] as string;

  // Fall back to Accept header content negotiation
  if (!version) {
    const accept = req.headers.accept ?? '';
    const match = accept.match(/application\/vnd\.example\.v(\d+)\+json/);
    version = match?.[1] ?? '1';   // default to v1
  }

  req.apiVersion = parseInt(version, 10);
  next();
}

app.use(resolveVersion);

app.get('/users/:id', (req: any, res) => {
  if (req.apiVersion >= 2) return userHandlerV2(req, res);
  return userHandlerV1(req, res);
});

Sunset & Deprecation Headers

Always announce deprecated versions via response headers so automated tooling and monitoring can alert developers before the cutoff date:

// Middleware: add deprecation headers to all v1 responses
function deprecationWarning(req: Request, res: Response, next: NextFunction) {
  // RFC 8594 Sunset header — machine-readable cutoff date
  res.set('Sunset', 'Sat, 01 Jan 2027 00:00:00 GMT');

  // Deprecation header — when deprecation was announced
  res.set('Deprecation', 'Mon, 01 Jul 2026 00:00:00 GMT');

  // Link to migration guide
  res.set('Link', [
    '<https://devplaybook.cc/api/migrate-v1-to-v2>; rel="successor-version"',
    '<https://devplaybook.cc/api/sunset>; rel="sunset"',
  ].join(', '));

  next();
}

// Apply only to v1 routes
app.use('/v1', deprecationWarning, v1Router);

// Response headers clients will see on every v1 call:
// Sunset: Sat, 01 Jan 2027 00:00:00 GMT
// Deprecation: Mon, 01 Jul 2026 00:00:00 GMT
// Link: <.../migrate-v1-to-v2>; rel="successor-version"

Versioning Strategy Comparison

Strategy Visibility REST Purity Browser Testable Routing
URL (/v1/) High Low (URL ≠ resource) Yes Very easy
Header (API-Version) Medium Medium Harder Moderate
Content Negotiation Low High Difficult Moderate
Query Param (?v=2) High Low Yes Easy
Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →

Frequently Asked Questions

What is the most common API versioning strategy?▼

URL path versioning (/v1/, /v2/) is by far the most widely used approach. It is immediately visible in browser address bars, easy to route at the load balancer or API gateway level, simple to document separately per version, and trivial for developers to understand. GitHub, Stripe, Twitter, and most major public APIs use URL versioning. The main downside is that it violates strict REST principles (the URL should identify a resource, not a contract version).

What is the difference between header versioning and content negotiation?▼

Header versioning uses a custom request header like API-Version: 2 to specify the desired version. Content negotiation uses the standard Accept header with a vendor MIME type: Accept: application/vnd.myapi.v2+json. Content negotiation is more RESTful because it uses standard HTTP mechanics, but it is harder to test in browsers, less visible in logs, and less intuitive for developers. Both keep URLs clean. Header versioning is a practical middle ground between URL versioning and full content negotiation.

How long should I maintain an old API version?▼

Industry practice is 6 to 24 months of parallel support after announcing a version's deprecation. The exact period depends on your user base — public APIs with many third-party integrations need longer windows. Communicate the sunset date clearly in documentation, in the Sunset and Deprecation headers on every response from the old version, and via direct email to registered API users.

What is the Sunset header?▼

The Sunset HTTP response header (RFC 8594) communicates the date after which a deprecated API version or endpoint will no longer be available. Example: Sunset: Sat, 01 Jan 2027 00:00:00 GMT. Combine it with a Link header pointing to migration documentation: Link: <https://devplaybook.cc/migrate>; rel='sunset'. Clients that inspect headers can automatically warn developers when they hit a sunsetted endpoint.

Should I version the entire API or individual endpoints?▼

Versioning the entire API (all endpoints move from /v1 to /v2 together) is simpler to manage but forces consumers to upgrade everything at once. Endpoint-level versioning lets you version only the changed resources, reducing migration scope for consumers. A pragmatic approach is API-level versioning with feature flags or additive changes for minor updates — increment the major API version only for breaking changes that affect multiple endpoints.

What counts as a breaking change that requires a version bump?▼

Breaking changes include: removing a field from a response, renaming a field, changing a field's type (e.g. string to number), changing HTTP status codes for existing operations, removing an endpoint, making an optional field required, and changing authentication mechanisms. Non-breaking (additive) changes that do not require a version bump: adding new optional fields to responses, adding new optional request parameters, adding new endpoints, and adding new enum values (with caution — strict enum parsers may break).