Files
flyer-crawler.projectium.com/docs/adr/0008-api-versioning-strategy.md
Torben Sorensen 0d4b028a66
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 21m49s
design fixup and docs + api versioning
2026-01-28 00:04:56 -08:00

14 KiB

ADR-008: API Versioning Strategy

Date: 2025-12-12

Status: Accepted (Phase 2 Complete - All Tasks Done)

Updated: 2026-01-27

Completion Note: Phase 2 fully complete including test path migration. All 23 integration test files updated to use /api/v1/ paths. Test suite improved from 274/348 to 345/348 passing (3 remain as todo/skipped for known issues unrelated to versioning).

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

Implementation Guide: API Versioning Infrastructure Developer Guide: API Versioning Developer Guide

  • Create version router factory (src/routes/versioned.ts)
  • Implement deprecation header middleware (src/middleware/deprecation.middleware.ts)
  • Add version detection to request context (src/middleware/apiVersion.middleware.ts)
  • Add version types to Express Request (src/types/express.d.ts)
  • Create version constants configuration (src/config/apiVersions.ts)
  • Update server.ts to use version router factory
  • Update swagger.ts for multi-server documentation
  • Add unit tests for version middleware
  • Add integration tests for versioned router
  • Document versioning patterns for developers
  • Migrate all test files to use /api/v1/ paths (23 files, ~70 occurrences)

Test Path Migration Summary (2026-01-27)

The final cleanup task for Phase 2 was completed by updating all integration test files to use versioned API paths:

Metric Value
Test files updated 23
Path occurrences changed ~70
Test failures resolved 71 (274 -> 345 passing)
Tests remaining todo/skipped 3 (known issues, not versioning-related)
Type check Passing
Versioning-specific tests 82/82 passing

Test Results After Migration:

  • Integration tests: 345/348 passing
  • Unit tests: 3,375/3,391 passing (16 pre-existing failures unrelated to versioning)

Unit Test Path Fix (2026-01-27)

Following the test path migration, 16 unit test failures were discovered and fixed. These failures were caused by error log messages using hardcoded /api/ paths instead of versioned /api/v1/ paths.

Root Cause: Error log messages in route handlers used hardcoded path strings like:

// INCORRECT - hardcoded path doesn't reflect actual request URL
req.log.error({ error }, 'Error in /api/flyers/:id:');

Solution: Updated to use req.originalUrl for dynamic path logging:

// CORRECT - uses actual request URL including version prefix
req.log.error({ error }, `Error in ${req.originalUrl.split('?')[0]}:`);

Files Modified:

File Changes
src/routes/recipe.routes.ts 3 error log statements updated
src/routes/stats.routes.ts 1 error log statement updated
src/routes/flyer.routes.ts 2 error logs + 2 test expectations
src/routes/personalization.routes.ts 3 error log statements updated

Test Results After Fix:

  • Unit tests: 3,382/3,391 passing (0 failures in fixed files)
  • Remaining 9 failures are pre-existing, unrelated issues (CSS/mocking)

Best Practice: See Error Logging Path Patterns for guidance on logging request paths in error handlers.

Migration Documentation: Test Path Migration Guide

Phase 3 Tasks (Future)

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