All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m56s
845 lines
24 KiB
Markdown
845 lines
24 KiB
Markdown
# 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
|
|
|
|
```
|
|
|
|
### 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
|
|
|
|
```
|
|
|
|
### 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)"
|
|
```
|