Files
flyer-crawler.projectium.com/docs/adr/0008-api-versioning-strategy.md
Torben Sorensen 2075ed199b Complete ADR-008 Phase 1: API Versioning Strategy
Implement URI-based API versioning with /api/v1 prefix across all routes.
This establishes a foundation for future API evolution and breaking changes.

Changes:
- server.ts: All routes mounted under /api/v1/ (15 route handlers)
- apiClient.ts: Base URL updated to /api/v1
- swagger.ts: OpenAPI server URL changed to /api/v1
- Redirect middleware: Added backwards compatibility for /api/* → /api/v1/*
- Tests: Updated 72 test files with versioned path assertions
- ADR documentation: Marked Phase 1 as complete (Accepted status)

Test fixes:
- apiClient.test.ts: 27 tests updated for /api/v1 paths
- user.routes.ts: 36 log messages updated to reflect versioned paths
- swagger.test.ts: 1 test updated for new server URL
- All integration/E2E tests updated for versioned endpoints

All Phase 1 acceptance criteria met:
✓ Routes use /api/v1/ prefix
✓ Frontend requests /api/v1/
✓ OpenAPI docs reflect /api/v1/
✓ Backwards compatibility via redirect middleware
✓ Tests pass with versioned paths

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-26 21:23:25 -08:00

11 KiB

ADR-008: API Versioning Strategy

Date: 2025-12-12

Status: Accepted (Phase 1 Complete)

Updated: 2026-01-26

Context

As the application grows, the API will need to evolve. Making breaking changes to existing endpoints can disrupt clients (e.g., a mobile app or the web frontend). The current routing has no formal versioning scheme.

Current State

As of January 2026, the API operates without explicit versioning:

  • All routes are mounted under /api/* (e.g., /api/flyers, /api/users/profile)
  • The frontend apiClient.ts uses API_BASE_URL = '/api' as the base
  • No version prefix exists in route paths
  • Breaking changes would immediately affect all consumers

Why Version Now?

  1. Future Mobile App: A native mobile app is planned, which will have slower update cycles than the web frontend
  2. Third-Party Integrations: Store partners may integrate with our API
  3. Deprecation Path: Need a clear way to deprecate and remove endpoints
  4. Documentation: OpenAPI documentation (ADR-018) should reflect versioned endpoints

Decision

We will adopt a URI-based versioning strategy for the API using a phased rollout approach. All routes will be prefixed with a version number (e.g., /api/v1/flyers).

Versioning Format

/api/v{MAJOR}/resource
  • MAJOR: Incremented for breaking changes (v1, v2, v3...)
  • Resource paths remain unchanged within a version

What Constitutes a Breaking Change?

The following changes require a new API version:

Change Type Breaking? Example
Remove endpoint Yes DELETE /api/v1/legacy-feature
Remove response field Yes Remove user.email from response
Change response field type Yes id: number to id: string
Change required request field Yes Make email required when it was optional
Rename endpoint Yes /users to /accounts
Add optional response field No Add user.avatar_url
Add optional request field No Add optional page parameter
Add new endpoint No Add /api/v1/new-feature
Fix bug in behavior No* Correct calculation error

*Bug fixes may warrant version increment if clients depend on the buggy behavior.

Implementation Phases

Phase 1: Namespace Migration (Current)

Goal: Add /v1/ prefix to all existing routes without behavioral changes.

Changes Required:

  1. server.ts: Update all route registrations

    // Before
    app.use('/api/auth', authRouter);
    
    // After
    app.use('/api/v1/auth', authRouter);
    
  2. apiClient.ts: Update base URL

    // Before
    const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
    
    // After
    const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1';
    
  3. swagger.ts: Update server definition

    servers: [
      {
        url: '/api/v1',
        description: 'API v1 server',
      },
    ],
    
  4. Redirect Middleware (optional): Support legacy clients

    // Redirect unversioned routes to v1
    app.use('/api/:resource', (req, res, next) => {
      if (req.params.resource !== 'v1') {
        return res.redirect(307, `/api/v1/${req.params.resource}${req.url}`);
      }
      next();
    });
    

Acceptance Criteria:

  • All existing functionality works at /api/v1/*
  • Frontend makes requests to /api/v1/*
  • OpenAPI documentation reflects /api/v1/* paths
  • Integration tests pass with new paths

Phase 2: Versioning Infrastructure

Goal: Build tooling to support multiple API versions.

Components:

  1. Version Router Factory

    // src/routes/versioned.ts
    export function createVersionedRoutes(version: 'v1' | 'v2') {
      const router = express.Router();
    
      if (version === 'v1') {
        router.use('/auth', authRouterV1);
        router.use('/users', userRouterV1);
        // ...
      } else if (version === 'v2') {
        router.use('/auth', authRouterV2);
        router.use('/users', userRouterV2);
        // ...
      }
    
      return router;
    }
    
  2. Version Detection Middleware

    // Extract version from URL and attach to request
    app.use('/api/:version', (req, res, next) => {
      req.apiVersion = req.params.version;
      next();
    });
    
  3. Deprecation Headers

    // Middleware to add deprecation headers
    function deprecateVersion(sunsetDate: string) {
      return (req, res, next) => {
        res.set('Deprecation', 'true');
        res.set('Sunset', sunsetDate);
        res.set('Link', '</api/v2>; rel="successor-version"');
        next();
      };
    }
    

Phase 3: Version 2 Support

Goal: Introduce v2 API when breaking changes are needed.

Triggers for v2:

  • Major schema changes (e.g., unified item model)
  • Response format overhaul
  • Authentication mechanism changes
  • Significant performance-driven restructuring

Parallel Support:

app.use('/api/v1', createVersionedRoutes('v1'));
app.use('/api/v2', createVersionedRoutes('v2'));

Migration Path

For Frontend (Web)

The web frontend is deployed alongside the API, so migration is straightforward:

  1. Update API_BASE_URL in apiClient.ts
  2. Update any hardcoded paths in tests
  3. Deploy frontend and backend together

For External Consumers

External consumers (mobile apps, partner integrations) need a transition period:

  1. Announcement: 30 days before deprecation of v(N-1)
  2. Deprecation Headers: Add headers 30 days before sunset
  3. Documentation: Maintain docs for both versions during transition
  4. Sunset: Remove v(N-1) after grace period

Deprecation Timeline

Version Status Sunset Date Notes
Unversioned /api/* Deprecated Phase 1 completion Redirect to v1
v1 Active TBD (when v2 releases) Current version

Support Policy

  • Current Version (v(N)): Full support, all features
  • Previous Version (v(N-1)): Security fixes only for 6 months after v(N) release
  • Older Versions: No support, endpoints return 410 Gone

Backwards Compatibility Strategy

Redirect Middleware

For a smooth transition, implement redirects from unversioned to versioned endpoints:

// src/middleware/versionRedirect.ts
import { Request, Response, NextFunction } from 'express';
import { logger } from '../services/logger.server';

/**
 * Middleware to redirect unversioned API requests to v1.
 * This provides backwards compatibility during the transition period.
 *
 * Example: /api/flyers -> /api/v1/flyers (307 Temporary Redirect)
 */
export function versionRedirectMiddleware(req: Request, res: Response, next: NextFunction) {
  const path = req.path;

  // Skip if already versioned
  if (path.startsWith('/v1') || path.startsWith('/v2')) {
    return next();
  }

  // Skip health checks and documentation
  if (path.startsWith('/health') || path.startsWith('/docs')) {
    return next();
  }

  // Log deprecation warning
  logger.warn({
    path: req.originalUrl,
    method: req.method,
    ip: req.ip,
  }, 'Unversioned API request - redirecting to v1');

  // Use 307 to preserve HTTP method
  const redirectUrl = `/api/v1${path}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`;
  return res.redirect(307, redirectUrl);
}

Response Versioning Headers

All API responses include version information:

// Middleware to add version headers
app.use('/api/v1', (req, res, next) => {
  res.set('X-API-Version', 'v1');
  next();
});

Consequences

Positive

  • Clear Evolution Path: Establishes a critical pattern for long-term maintainability
  • Client Protection: Allows the API to evolve without breaking existing clients
  • Parallel Development: Can develop v2 features while maintaining v1 stability
  • Documentation Clarity: Each version has its own complete documentation
  • Graceful Deprecation: Clients have clear timelines and migration paths

Negative

  • Routing Complexity: Adds complexity to the routing setup
  • Code Duplication: May need to maintain multiple versions of handlers
  • Testing Overhead: Tests may need to cover multiple versions
  • Documentation Maintenance: Must keep docs for multiple versions in sync

Mitigation

  • Use shared business logic with version-specific adapters
  • Automate deprecation header addition
  • Generate versioned OpenAPI specs from code
  • Clear internal guidelines on when to increment versions

Key Files

File Purpose
server.ts Route registration with version prefixes
src/services/apiClient.ts Frontend API base URL configuration
src/config/swagger.ts OpenAPI server URL and version info
src/routes/*.routes.ts Individual route handlers
src/middleware/versionRedirect.ts Backwards compatibility redirects (Phase 1)
  • ADR-003 - Input Validation (consistent across versions)
  • ADR-018 - API Documentation Strategy (versioned OpenAPI specs)
  • ADR-028 - Response Standardization (envelope pattern applies to all versions)
  • ADR-016 - Security Hardening (applies to all versions)

Implementation Checklist

Phase 1 Tasks

  • Update server.ts to mount all routes under /api/v1/
  • Update src/services/apiClient.ts API_BASE_URL to /api/v1
  • Update src/config/swagger.ts server URL to /api/v1
  • Add redirect middleware for unversioned requests
  • Update integration tests to use versioned paths
  • Update API documentation examples (Swagger server URL updated)
  • Verify all health checks work at /api/v1/health/*

Phase 2 Tasks (Future)

  • Create version router factory
  • Implement deprecation header middleware
  • Add version detection to request context
  • Document versioning patterns for developers

Phase 3 Tasks (Future)

  • Identify breaking changes requiring v2
  • Create v2 route handlers
  • Set deprecation timeline for v1
  • Migrate documentation to multi-version format