# 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<ApiVersion, VersionConfig> = {
  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<ApiVersion, VersionConfig> = {
  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<ApiVersion, VersionConfig> = {
  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: </api/v2>; 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: </api/v2>; 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', '</api/v2>; 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: </api/v2>; 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)"
```
