Skip to main content

ADR-0026: URL-Based API Versioning

Status

Accepted - 2025-01-26


Context

TVL Platform APIs will evolve over time, requiring backward compatibility for existing clients.


Decision

URL-based versioning (/api/v1/, /api/v2/) for API endpoints.

Rationale

  1. Explicit: Version visible in URL
  2. Cacheable: Different URLs = different cache entries
  3. Simple: Easy to route and test
  4. Industry Standard: Used by Stripe, Twilio, GitHub

Alternatives Considered

Alternative 1: Header Versioning

Rejected - Harder to test (can't test in browser), breaks caching

Alternative 2: Query Parameter

Rejected - Breaks caching, non-standard

Alternative 3: Content Negotiation

Rejected - Complex, requires custom Accept headers


URL Structure

/api/v1/bookings           # Version 1
/api/v2/bookings # Version 2 (breaking changes)
/api/v1/properties/:id # Version 1, specific resource

Versioning Strategy

When to Create New Version

Create new version (v2) when:

  • ✅ Removing fields from response
  • ✅ Changing field types (string → number)
  • ✅ Changing required fields
  • ✅ Changing validation rules (stricter)

Backward compatible (stay in v1) when:

  • ✅ Adding new optional fields
  • ✅ Adding new endpoints
  • ✅ Relaxing validation rules

Deprecation Policy

  • Support N-1 versions for 12 months
  • Announce deprecation 6 months in advance
  • Sunset header indicates deprecation
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: true
Link: </api/v2/bookings>; rel="alternate"

Implementation

// src/routes/v1/bookings.ts
import { FastifyPluginAsync } from 'fastify';

const v1Routes: FastifyPluginAsync = async (app) => {
app.get('/bookings', async (req, reply) => {
// Version 1 implementation
});
};

// src/routes/v2/bookings.ts
const v2Routes: FastifyPluginAsync = async (app) => {
app.get('/bookings', async (req, reply) => {
// Version 2 implementation (breaking changes)
});
};

// src/server.ts
app.register(v1Routes, { prefix: '/api/v1' });
app.register(v2Routes, { prefix: '/api/v2' });

Version Lifecycle

v1 (Active) → v2 Released → v1 Deprecated (12 mo) → v1 Sunset

References