|
| 1 | +/** |
| 2 | + * API Versioning configuration and lifecycle. |
| 3 | + * Supports multi-version API with backward compatibility, deprecation, and sunset policies. |
| 4 | + */ |
| 5 | + |
| 6 | +export type VersionStatus = 'stable' | 'deprecated' | 'sunset'; |
| 7 | + |
| 8 | +export interface VersionLifecycle { |
| 9 | + version: string; |
| 10 | + status: VersionStatus; |
| 11 | + /** RFC 3339 date when this version was released */ |
| 12 | + releasedAt: string; |
| 13 | + /** RFC 3339 date when deprecation was announced (if status is deprecated or sunset) */ |
| 14 | + deprecatedAt?: string; |
| 15 | + /** RFC 3339 date after which the version will be removed (sunset) */ |
| 16 | + sunsetAt?: string; |
| 17 | + /** URL to migration guide or changelog */ |
| 18 | + migrationGuideUrl?: string; |
| 19 | + /** Human-readable deprecation message */ |
| 20 | + deprecationMessage?: string; |
| 21 | +} |
| 22 | + |
| 23 | +export interface ApiVersioningConfig { |
| 24 | + /** Currently supported API versions (e.g. ['v1', 'v2']) */ |
| 25 | + supportedVersions: string[]; |
| 26 | + /** Default version when client does not specify one */ |
| 27 | + defaultVersion: string; |
| 28 | + /** Latest version (used for /api without version prefix) */ |
| 29 | + latestVersion: string; |
| 30 | + /** Version lifecycle and deprecation metadata */ |
| 31 | + lifecycle: Record<string, VersionLifecycle>; |
| 32 | + /** Base URL for versioned docs (e.g. https://docs.nepa.example.com/api) */ |
| 33 | + docsBaseUrl?: string; |
| 34 | +} |
| 35 | + |
| 36 | +const LIFECYCLE: Record<string, VersionLifecycle> = { |
| 37 | + v1: { |
| 38 | + version: 'v1', |
| 39 | + status: 'stable', |
| 40 | + releasedAt: '2024-01-01T00:00:00Z', |
| 41 | + migrationGuideUrl: '/docs/api-versioning#migrating-v1-to-v2', |
| 42 | + }, |
| 43 | + v2: { |
| 44 | + version: 'v2', |
| 45 | + status: 'stable', |
| 46 | + releasedAt: '2024-06-01T00:00:00Z', |
| 47 | + migrationGuideUrl: '/docs/api-versioning', |
| 48 | + }, |
| 49 | +}; |
| 50 | + |
| 51 | +export const apiVersioningConfig: ApiVersioningConfig = { |
| 52 | + supportedVersions: ['v1', 'v2'], |
| 53 | + defaultVersion: 'v2', |
| 54 | + latestVersion: 'v2', |
| 55 | + lifecycle: LIFECYCLE, |
| 56 | + docsBaseUrl: process.env.API_DOCS_BASE_URL || '/api/docs', |
| 57 | +}; |
| 58 | + |
| 59 | +/** |
| 60 | + * Get lifecycle for a version. Returns undefined if version is unknown. |
| 61 | + */ |
| 62 | +export function getVersionLifecycle(version: string): VersionLifecycle | undefined { |
| 63 | + const normalized = version.startsWith('v') ? version : `v${version}`; |
| 64 | + return apiVersioningConfig.lifecycle[normalized]; |
| 65 | +} |
| 66 | + |
| 67 | +/** |
| 68 | + * Check if a version is supported (exists and not sunset). |
| 69 | + */ |
| 70 | +export function isVersionSupported(version: string): boolean { |
| 71 | + const normalized = version.startsWith('v') ? version : `v${version}`; |
| 72 | + if (!apiVersioningConfig.supportedVersions.includes(normalized)) return false; |
| 73 | + const lifecycle = getVersionLifecycle(normalized); |
| 74 | + return lifecycle ? lifecycle.status !== 'sunset' : false; |
| 75 | +} |
| 76 | + |
| 77 | +/** |
| 78 | + * Get HTTP headers for deprecation/sunset (RFC 8594-style). |
| 79 | + */ |
| 80 | +export function getDeprecationHeaders(version: string): Record<string, string> { |
| 81 | + const lifecycle = getVersionLifecycle(version); |
| 82 | + if (!lifecycle || lifecycle.status === 'stable') return {}; |
| 83 | + |
| 84 | + const headers: Record<string, string> = {}; |
| 85 | + if (lifecycle.status === 'deprecated' || lifecycle.status === 'sunset') { |
| 86 | + headers['Deprecation'] = 'true'; |
| 87 | + if (lifecycle.sunsetAt) { |
| 88 | + headers['Sunset'] = lifecycle.sunsetAt; |
| 89 | + } |
| 90 | + if (lifecycle.migrationGuideUrl) { |
| 91 | + headers['Link'] = `<${lifecycle.migrationGuideUrl}>; rel="deprecation"; type="text/html"`; |
| 92 | + } |
| 93 | + } |
| 94 | + return headers; |
| 95 | +} |
0 commit comments