# API Versioning Developer Guide **Status**: Complete (Phase 2) **Last Updated**: 2026-01-27 **Implements**: ADR-008 Phase 2 **Architecture**: [api-versioning-infrastructure.md](../architecture/api-versioning-infrastructure.md) This guide covers the API versioning infrastructure for the Flyer Crawler application. It explains how versioning works, how to add new versions, and how to deprecate old ones. ## Implementation Status | Component | Status | Tests | | ------------------------------ | -------- | -------------------- | | Version Constants | Complete | Unit tests | | Version Detection Middleware | Complete | 25 unit tests | | Deprecation Headers Middleware | Complete | 30 unit tests | | Version Router Factory | Complete | Integration tests | | Server Integration | Complete | 48 integration tests | | Developer Documentation | Complete | This guide | **Total Tests**: 82 versioning-specific tests (100% passing) --- ## Table of Contents 1. [Overview](#overview) 2. [Architecture](#architecture) 3. [Key Concepts](#key-concepts) 4. [Developer Workflows](#developer-workflows) 5. [Version Headers](#version-headers) 6. [Testing Versioned Endpoints](#testing-versioned-endpoints) 7. [Migration Guide: v1 to v2](#migration-guide-v1-to-v2) 8. [Troubleshooting](#troubleshooting) 9. [Related Documentation](#related-documentation) --- ## Overview The API uses URI-based versioning with the format `/api/v{MAJOR}/resource`. All endpoints are accessible at versioned paths like `/api/v1/flyers` or `/api/v2/users`. ### Current Version Status | Version | Status | Description | | ------- | ------ | ------------------------------------- | | v1 | Active | Current production version | | v2 | Active | Future version (infrastructure ready) | ### Key Features - **Automatic version detection** from URL path - **RFC 8594 compliant deprecation headers** when versions are deprecated - **Backwards compatibility** via 301 redirects from unversioned paths - **Version-aware request context** for conditional logic in handlers - **Centralized configuration** for version lifecycle management --- ## Architecture ### Request Flow ```text Client Request: GET /api/v1/flyers | v +------+-------+ | server.ts | | - Redirect | | middleware | +------+-------+ | v +------+-------+ | createApi | | Router() | +------+-------+ | v +------+-------+ | detectApi | | Version | | middleware | +------+-------+ | req.apiVersion = 'v1' v +------+-------+ | Versioned | | Router | | (v1) | +------+-------+ | v +------+-------+ | addDepreca | | tionHeaders | | middleware | +------+-------+ | X-API-Version: v1 v +------+-------+ | Domain | | Router | | (flyers) | +------+-------+ | v Response ``` ### Component Overview | Component | File | Purpose | | ------------------- | ------------------------------------------ | ----------------------------------------------------- | | Version Constants | `src/config/apiVersions.ts` | Type definitions, version configs, utility functions | | Version Detection | `src/middleware/apiVersion.middleware.ts` | Extract version from URL, validate, attach to request | | Deprecation Headers | `src/middleware/deprecation.middleware.ts` | Add RFC 8594 headers for deprecated versions | | Router Factory | `src/routes/versioned.ts` | Create version-specific Express routers | | Type Extensions | `src/types/express.d.ts` | Add `apiVersion` and `versionDeprecation` to Request | --- ## Key Concepts ### 1. Version Configuration All version definitions live in `src/config/apiVersions.ts`: ```typescript // src/config/apiVersions.ts // Supported versions as a const tuple export const API_VERSIONS = ['v1', 'v2'] as const; // Union type: 'v1' | 'v2' export type ApiVersion = (typeof API_VERSIONS)[number]; // Version lifecycle status export type VersionStatus = 'active' | 'deprecated' | 'sunset'; // Configuration for each version export const VERSION_CONFIGS: Record = { v1: { version: 'v1', status: 'active', }, v2: { version: 'v2', status: 'active', }, }; ``` ### 2. Version Detection The `detectApiVersion` middleware extracts the version from `req.params.version` and validates it: ```typescript // How it works (src/middleware/apiVersion.middleware.ts) // For valid versions: // GET /api/v1/flyers -> req.apiVersion = 'v1' // For invalid versions: // GET /api/v99/flyers -> 404 with UNSUPPORTED_VERSION error ``` ### 3. Request Context After middleware runs, the request object has version information: ```typescript // In any route handler router.get('/flyers', async (req, res) => { // Access the detected version const version = req.apiVersion; // 'v1' | 'v2' // Check deprecation status if (req.versionDeprecation?.deprecated) { req.log.warn( { sunset: req.versionDeprecation.sunsetDate, }, 'Client using deprecated API', ); } // Version-specific behavior if (req.apiVersion === 'v2') { return sendSuccess(res, transformV2(data)); } return sendSuccess(res, data); }); ``` ### 4. Route Registration Routes are registered in `src/routes/versioned.ts` with version availability: ```typescript // src/routes/versioned.ts export const ROUTES: RouteRegistration[] = [ { path: 'auth', router: authRouter, description: 'Authentication routes', // Available in all versions (no versions array) }, { path: 'flyers', router: flyerRouter, description: 'Flyer management', // Available in all versions }, { path: 'new-feature', router: newFeatureRouter, description: 'New feature only in v2', versions: ['v2'], // Only available in v2 }, ]; ``` --- ## Developer Workflows ### Adding a New API Version (e.g., v3) **Step 1**: Add version to constants (`src/config/apiVersions.ts`) ```typescript // Before export const API_VERSIONS = ['v1', 'v2'] as const; // After export const API_VERSIONS = ['v1', 'v2', 'v3'] as const; // Add configuration export const VERSION_CONFIGS: Record = { v1: { version: 'v1', status: 'active' }, v2: { version: 'v2', status: 'active' }, v3: { version: 'v3', status: 'active' }, // NEW }; ``` **Step 2**: Router cache auto-updates (no changes needed) The versioned router cache in `src/routes/versioned.ts` automatically creates routers for all versions defined in `API_VERSIONS`. **Step 3**: Update OpenAPI documentation (`src/config/swagger.ts`) ```typescript servers: [ { url: '/api/v1', description: 'API v1' }, { url: '/api/v2', description: 'API v2' }, { url: '/api/v3', description: 'API v3 (New)' }, // NEW ], ``` **Step 4**: Test the new version ```bash # In dev container podman exec -it flyer-crawler-dev npm test # Manual verification curl -i http://localhost:3001/api/v3/health # Should return 200 with X-API-Version: v3 header ``` ### Marking a Version as Deprecated **Step 1**: Update version config (`src/config/apiVersions.ts`) ```typescript export const VERSION_CONFIGS: Record = { v1: { version: 'v1', status: 'deprecated', // Changed from 'active' sunsetDate: '2027-01-01T00:00:00Z', // When it will be removed successorVersion: 'v2', // Migration target }, v2: { version: 'v2', status: 'active', }, }; ``` **Step 2**: Verify deprecation headers ```bash curl -I http://localhost:3001/api/v1/health # Expected headers: # X-API-Version: v1 # Deprecation: true # Sunset: 2027-01-01T00:00:00Z # Link: ; rel="successor-version" # X-API-Deprecation-Notice: API v1 is deprecated and will be sunset... ``` **Step 3**: Monitor deprecation usage Check logs for `Deprecated API version accessed` messages with context about which clients are still using deprecated versions. ### Adding Version-Specific Routes **Scenario**: Add a new endpoint only available in v2+ **Step 1**: Create the route handler (new or existing file) ```typescript // src/routes/newFeature.routes.ts import { Router } from 'express'; import { sendSuccess } from '../utils/apiResponse'; const router = Router(); router.get('/', async (req, res) => { // This endpoint only exists in v2+ sendSuccess(res, { feature: 'new-feature-data' }); }); export default router; ``` **Step 2**: Register with version restriction (`src/routes/versioned.ts`) ```typescript import newFeatureRouter from './newFeature.routes'; export const ROUTES: RouteRegistration[] = [ // ... existing routes ... { path: 'new-feature', router: newFeatureRouter, description: 'New feature only available in v2+', versions: ['v2'], // Not available in v1 }, ]; ``` **Step 3**: Verify route availability ```bash # v1 - should return 404 curl -i http://localhost:3001/api/v1/new-feature # HTTP/1.1 404 Not Found # v2 - should work curl -i http://localhost:3001/api/v2/new-feature # HTTP/1.1 200 OK # X-API-Version: v2 ``` ### Adding Version-Specific Behavior in Existing Routes For routes that exist in multiple versions but behave differently: ```typescript // src/routes/flyer.routes.ts router.get('/:id', async (req, res) => { const flyer = await flyerService.getFlyer(req.params.id, req.log); // Different response format per version if (req.apiVersion === 'v2') { // v2 returns expanded store data return sendSuccess(res, { ...flyer, store: await storeService.getStore(flyer.store_id, req.log), }); } // v1 returns just the flyer return sendSuccess(res, flyer); }); ``` --- ## Version Headers ### Response Headers All versioned API responses include these headers: | Header | Always Present | Description | | -------------------------- | ------------------ | ------------------------------------------------------- | | `X-API-Version` | Yes | The API version handling the request | | `Deprecation` | Only if deprecated | `true` when version is deprecated | | `Sunset` | Only if configured | ISO 8601 date when version will be removed | | `Link` | Only if configured | URL to successor version with `rel="successor-version"` | | `X-API-Deprecation-Notice` | Only if deprecated | Human-readable deprecation message | ### Example: Active Version Response ```http HTTP/1.1 200 OK X-API-Version: v2 Content-Type: application/json {"success":true,"data":{...}} ``` ### Example: Deprecated Version Response ```http HTTP/1.1 200 OK X-API-Version: v1 Deprecation: true Sunset: 2027-01-01T00:00:00Z Link: ; rel="successor-version" X-API-Deprecation-Notice: API v1 is deprecated and will be sunset on 2027-01-01T00:00:00Z. Please migrate to v2. Content-Type: application/json {"success":true,"data":{...}} ``` ### RFC Compliance The deprecation headers follow these standards: - **RFC 8594**: The "Sunset" HTTP Header Field - **draft-ietf-httpapi-deprecation-header**: The "Deprecation" HTTP Header Field - **RFC 8288**: Web Linking (for `rel="successor-version"`) --- ## Testing Versioned Endpoints ### Unit Testing Middleware See test files for patterns: - `src/middleware/apiVersion.middleware.test.ts` - `src/middleware/deprecation.middleware.test.ts` **Testing version detection**: ```typescript // src/middleware/apiVersion.middleware.test.ts import { detectApiVersion } from './apiVersion.middleware'; import { createMockRequest } from '../tests/utils/createMockRequest'; describe('detectApiVersion', () => { it('should extract v1 from req.params.version', () => { const mockRequest = createMockRequest({ params: { version: 'v1' }, }); const mockResponse = { status: vi.fn().mockReturnThis(), json: vi.fn() }; const mockNext = vi.fn(); detectApiVersion(mockRequest, mockResponse, mockNext); expect(mockRequest.apiVersion).toBe('v1'); expect(mockNext).toHaveBeenCalled(); }); it('should return 404 for invalid version', () => { const mockRequest = createMockRequest({ params: { version: 'v99' }, }); const mockResponse = { status: vi.fn().mockReturnThis(), json: vi.fn(), }; const mockNext = vi.fn(); detectApiVersion(mockRequest, mockResponse, mockNext); expect(mockNext).not.toHaveBeenCalled(); expect(mockResponse.status).toHaveBeenCalledWith(404); }); }); ``` **Testing deprecation headers**: ```typescript // src/middleware/deprecation.middleware.test.ts import { addDeprecationHeaders } from './deprecation.middleware'; import { VERSION_CONFIGS } from '../config/apiVersions'; describe('addDeprecationHeaders', () => { beforeEach(() => { // Mark v1 as deprecated for test VERSION_CONFIGS.v1 = { version: 'v1', status: 'deprecated', sunsetDate: '2027-01-01T00:00:00Z', successorVersion: 'v2', }; }); it('should add all deprecation headers', () => { const setHeader = vi.fn(); const middleware = addDeprecationHeaders('v1'); middleware(mockRequest, { set: setHeader }, mockNext); expect(setHeader).toHaveBeenCalledWith('Deprecation', 'true'); expect(setHeader).toHaveBeenCalledWith('Sunset', '2027-01-01T00:00:00Z'); expect(setHeader).toHaveBeenCalledWith('Link', '; rel="successor-version"'); }); }); ``` ### Integration Testing **Test versioned endpoints**: ```typescript import request from 'supertest'; import app from '../../server'; describe('API Versioning Integration', () => { it('should return X-API-Version header for v1', async () => { const response = await request(app).get('/api/v1/health').expect(200); expect(response.headers['x-api-version']).toBe('v1'); }); it('should return 404 for unsupported version', async () => { const response = await request(app).get('/api/v99/health').expect(404); expect(response.body.error.code).toBe('UNSUPPORTED_VERSION'); }); it('should redirect unversioned paths to v1', async () => { const response = await request(app).get('/api/health').expect(301); expect(response.headers.location).toBe('/api/v1/health'); }); }); ``` ### Running Tests ```bash # Run all tests in container (required) podman exec -it flyer-crawler-dev npm test # Run only middleware tests podman exec -it flyer-crawler-dev npm test -- apiVersion podman exec -it flyer-crawler-dev npm test -- deprecation # Type check podman exec -it flyer-crawler-dev npm run type-check ``` --- ## Migration Guide: v1 to v2 When v2 is introduced with breaking changes, follow this migration process. ### For API Consumers (Frontend/Mobile) **Step 1**: Check current API version usage ```typescript // Frontend apiClient.ts const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'; ``` **Step 2**: Monitor deprecation headers When v1 is deprecated, responses will include: ```http Deprecation: true Sunset: 2027-01-01T00:00:00Z Link: ; rel="successor-version" ``` **Step 3**: Update to v2 ```typescript // Change API base URL const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v2'; ``` **Step 4**: Handle response format changes If v2 changes response formats, update your type definitions and parsing logic: ```typescript // v1 response interface FlyerResponseV1 { id: number; store_id: number; } // v2 response (example: includes embedded store) interface FlyerResponseV2 { id: string; // Changed to UUID store: { id: string; name: string; }; } ``` ### For Backend Developers **Step 1**: Create v2-specific handlers (if needed) For breaking changes, create version-specific route files: ```text src/routes/ flyer.routes.ts # Shared/v1 handlers flyer.v2.routes.ts # v2-specific handlers (if significantly different) ``` **Step 2**: Register version-specific routes ```typescript // src/routes/versioned.ts export const ROUTES: RouteRegistration[] = [ { path: 'flyers', router: flyerRouter, description: 'Flyer routes (v1)', versions: ['v1'], }, { path: 'flyers', router: flyerRouterV2, description: 'Flyer routes (v2 with breaking changes)', versions: ['v2'], }, ]; ``` **Step 3**: Document changes Update OpenAPI documentation to reflect v2 changes and mark v1 as deprecated. ### Timeline Example | Date | Action | | ---------- | ------------------------------------------ | | T+0 | v2 released, v1 marked deprecated | | T+0 | Deprecation headers added to v1 responses | | T+30 days | Sunset warning emails to known integrators | | T+90 days | v1 returns 410 Gone | | T+120 days | v1 code removed | --- ## Troubleshooting ### Issue: "UNSUPPORTED_VERSION" Error **Symptom**: Request to `/api/v3/...` returns 404 with `UNSUPPORTED_VERSION` **Cause**: Version `v3` is not defined in `API_VERSIONS` **Solution**: Add the version to `src/config/apiVersions.ts`: ```typescript export const API_VERSIONS = ['v1', 'v2', 'v3'] as const; export const VERSION_CONFIGS = { // ... v3: { version: 'v3', status: 'active' }, }; ``` ### Issue: Missing X-API-Version Header **Symptom**: Response doesn't include `X-API-Version` header **Cause**: Request didn't go through versioned router **Solution**: Ensure the route is registered in `src/routes/versioned.ts` and mounted under `/api/:version` ### Issue: Deprecation Headers Not Appearing **Symptom**: Deprecated version works but no deprecation headers **Cause**: Version status not set to `'deprecated'` in config **Solution**: Update `VERSION_CONFIGS`: ```typescript v1: { version: 'v1', status: 'deprecated', // Must be 'deprecated', not 'active' sunsetDate: '2027-01-01T00:00:00Z', successorVersion: 'v2', }, ``` ### Issue: Route Available in Wrong Version **Symptom**: Route works in v1 but should only be in v2 **Cause**: Missing `versions` restriction in route registration **Solution**: Add `versions` array: ```typescript { path: 'new-feature', router: newFeatureRouter, versions: ['v2'], // Add this to restrict availability }, ``` ### Issue: Unversioned Paths Not Redirecting **Symptom**: `/api/flyers` returns 404 instead of redirecting to `/api/v1/flyers` **Cause**: Redirect middleware order issue in `server.ts` **Solution**: Ensure redirect middleware is mounted BEFORE `createApiRouter()`: ```typescript // server.ts - correct order app.use('/api', redirectMiddleware); // First app.use('/api', createApiRouter()); // Second ``` ### Issue: TypeScript Errors on req.apiVersion **Symptom**: `Property 'apiVersion' does not exist on type 'Request'` **Cause**: Type extensions not being picked up **Solution**: Ensure `src/types/express.d.ts` is included in tsconfig: ```json { "compilerOptions": { "typeRoots": ["./node_modules/@types", "./src/types"] }, "include": ["src/**/*"] } ``` ### Issue: Router Cache Stale After Config Change **Symptom**: Version behavior doesn't update after changing `VERSION_CONFIGS` **Cause**: Routers are cached at startup **Solution**: Use `refreshRouterCache()` or restart the server: ```typescript import { refreshRouterCache } from './src/routes/versioned'; // After config changes refreshRouterCache(); ``` --- ## Related Documentation ### Architecture Decision Records | ADR | Title | | ------------------------------------------------------------------------ | ---------------------------- | | [ADR-008](../adr/0008-api-versioning-strategy.md) | API Versioning Strategy | | [ADR-003](../adr/0003-standardized-input-validation-using-middleware.md) | Input Validation | | [ADR-028](../adr/0028-api-response-standardization.md) | API Response Standardization | | [ADR-018](../adr/0018-api-documentation-strategy.md) | API Documentation Strategy | ### Implementation Files | File | Description | | -------------------------------------------------------------------------------------------- | ---------------------------- | | [`src/config/apiVersions.ts`](../../src/config/apiVersions.ts) | Version constants and config | | [`src/middleware/apiVersion.middleware.ts`](../../src/middleware/apiVersion.middleware.ts) | Version detection | | [`src/middleware/deprecation.middleware.ts`](../../src/middleware/deprecation.middleware.ts) | Deprecation headers | | [`src/routes/versioned.ts`](../../src/routes/versioned.ts) | Router factory | | [`src/types/express.d.ts`](../../src/types/express.d.ts) | Request type extensions | | [`server.ts`](../../server.ts) | Application entry point | ### Test Files | File | Description | | ------------------------------------------------------------------------------------------------------ | ------------------------ | | [`src/middleware/apiVersion.middleware.test.ts`](../../src/middleware/apiVersion.middleware.test.ts) | Version detection tests | | [`src/middleware/deprecation.middleware.test.ts`](../../src/middleware/deprecation.middleware.test.ts) | Deprecation header tests | ### External References - [RFC 8594: The "Sunset" HTTP Header Field](https://datatracker.ietf.org/doc/html/rfc8594) - [draft-ietf-httpapi-deprecation-header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-deprecation-header/) - [RFC 8288: Web Linking](https://datatracker.ietf.org/doc/html/rfc8288) --- ## Quick Reference ### Files to Modify for Common Tasks | Task | Files | | ------------------------------ | ---------------------------------------------------- | | Add new version | `src/config/apiVersions.ts`, `src/config/swagger.ts` | | Deprecate version | `src/config/apiVersions.ts` | | Add version-specific route | `src/routes/versioned.ts` | | Version-specific handler logic | Route file (e.g., `src/routes/flyer.routes.ts`) | ### Key Functions ```typescript // Check if version is valid isValidApiVersion('v1'); // true isValidApiVersion('v99'); // false // Get version from request with fallback getRequestApiVersion(req); // Returns 'v1' | 'v2' // Check if request has valid version hasApiVersion(req); // boolean // Get deprecation info getVersionDeprecation('v1'); // { deprecated: false, ... } ``` ### Commands ```bash # Run all tests podman exec -it flyer-crawler-dev npm test # Type check podman exec -it flyer-crawler-dev npm run type-check # Check version headers manually curl -I http://localhost:3001/api/v1/health # Test deprecation (after marking v1 deprecated) curl -I http://localhost:3001/api/v1/health | grep -E "(Deprecation|Sunset|Link|X-API)" ```