← back to Wine Finder Next
lib/apiVersioning.ts
357 lines
// API Versioning System
// Handles multiple API versions with backward compatibility
import { NextRequest, NextResponse } from 'next/server';
interface APIVersion {
version: string;
deprecated: boolean;
deprecationDate?: Date;
sunsetDate?: Date;
changes: string[];
}
interface VersionedEndpoint {
path: string;
versions: Map<string, Function>;
defaultVersion: string;
}
// API version registry
const API_VERSIONS: Map<string, APIVersion> = new Map([
['v1', {
version: 'v1',
deprecated: true,
deprecationDate: new Date('2024-01-01'),
sunsetDate: new Date('2025-06-01'),
changes: ['Initial API version']
}],
['v2', {
version: 'v2',
deprecated: false,
changes: [
'Enhanced security features',
'Improved rate limiting',
'Better error messages',
'Added CSRF protection',
'Session management improvements'
]
}],
['v3', {
version: 'v3',
deprecated: false,
changes: [
'GraphQL support',
'WebSocket real-time updates',
'Advanced filtering options',
'Batch operations support'
]
}]
]);
// Current and default versions
const CURRENT_VERSION = 'v3';
const DEFAULT_VERSION = 'v2';
const MINIMUM_SUPPORTED_VERSION = 'v1';
// Versioned endpoints registry
const versionedEndpoints = new Map<string, VersionedEndpoint>();
// Extract API version from request
export function extractAPIVersion(request: NextRequest): {
version: string;
source: 'header' | 'path' | 'query' | 'default';
} {
// 1. Check Accept header (preferred)
const acceptHeader = request.headers.get('accept');
if (acceptHeader) {
const versionMatch = acceptHeader.match(/application\/vnd\.winedao\.([^+]+)\+json/);
if (versionMatch) {
return { version: versionMatch[1], source: 'header' };
}
}
// 2. Check custom version header
const versionHeader = request.headers.get('x-api-version');
if (versionHeader) {
return { version: versionHeader, source: 'header' };
}
// 3. Check URL path
const pathMatch = request.nextUrl.pathname.match(/\/api\/(v\d+)\//);
if (pathMatch) {
return { version: pathMatch[1], source: 'path' };
}
// 4. Check query parameter
const queryVersion = request.nextUrl.searchParams.get('api_version');
if (queryVersion) {
return { version: queryVersion, source: 'query' };
}
// 5. Default version
return { version: DEFAULT_VERSION, source: 'default' };
}
// Validate API version
export function validateAPIVersion(version: string): {
valid: boolean;
error?: string;
warnings?: string[];
} {
const versionInfo = API_VERSIONS.get(version);
if (!versionInfo) {
return {
valid: false,
error: `API version ${version} is not supported. Available versions: ${Array.from(API_VERSIONS.keys()).join(', ')}`
};
}
const warnings: string[] = [];
if (versionInfo.deprecated) {
warnings.push(`API version ${version} is deprecated since ${versionInfo.deprecationDate?.toISOString()}`);
if (versionInfo.sunsetDate) {
const daysUntilSunset = Math.ceil(
(versionInfo.sunsetDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
if (daysUntilSunset <= 0) {
return {
valid: false,
error: `API version ${version} has been sunset and is no longer available`
};
}
warnings.push(`This version will be sunset in ${daysUntilSunset} days (${versionInfo.sunsetDate.toISOString()})`);
}
}
return { valid: true, warnings };
}
// Version compatibility middleware
export function versionMiddleware(
request: NextRequest,
handler: (req: NextRequest, version: string) => Promise<NextResponse>
): Promise<NextResponse> {
const { version, source } = extractAPIVersion(request);
const validation = validateAPIVersion(version);
if (!validation.valid) {
return Promise.resolve(
NextResponse.json({
success: false,
error: validation.error,
supportedVersions: Array.from(API_VERSIONS.keys()),
currentVersion: CURRENT_VERSION
}, { status: 400 })
);
}
// Add version headers to response
return handler(request, version).then(response => {
response.headers.set('x-api-version', version);
response.headers.set('x-api-version-source', source);
if (validation.warnings && validation.warnings.length > 0) {
response.headers.set('x-api-deprecation', validation.warnings.join('; '));
}
// Add supported versions header
response.headers.set('x-api-supported-versions', Array.from(API_VERSIONS.keys()).join(', '));
return response;
});
}
// Register versioned endpoint
export function registerVersionedEndpoint(
path: string,
versions: Record<string, Function>,
defaultVersion?: string
): void {
const versionMap = new Map(Object.entries(versions));
versionedEndpoints.set(path, {
path,
versions: versionMap,
defaultVersion: defaultVersion || DEFAULT_VERSION
});
}
// Get handler for specific version
export function getVersionedHandler(
path: string,
version: string
): Function | undefined {
const endpoint = versionedEndpoints.get(path);
if (!endpoint) {
return undefined;
}
// Try exact version match
let handler = endpoint.versions.get(version);
if (handler) {
return handler;
}
// Try fallback to previous version
const versionNumber = parseInt(version.replace('v', ''));
for (let v = versionNumber - 1; v >= 1; v--) {
handler = endpoint.versions.get(`v${v}`);
if (handler) {
console.warn(`[API Version] Falling back from ${version} to v${v} for ${path}`);
return handler;
}
}
// Use default version
return endpoint.versions.get(endpoint.defaultVersion);
}
// Transform response for version compatibility
export function transformResponseForVersion(
data: any,
fromVersion: string,
toVersion: string
): any {
// Handle version-specific transformations
if (fromVersion === 'v3' && toVersion === 'v2') {
// Remove v3-specific fields
if (Array.isArray(data)) {
return data.map(item => transformV3toV2(item));
}
return transformV3toV2(data);
}
if (fromVersion === 'v2' && toVersion === 'v1') {
// Remove v2-specific fields
if (Array.isArray(data)) {
return data.map(item => transformV2toV1(item));
}
return transformV2toV1(data);
}
return data;
}
// Version-specific transformations
function transformV3toV2(data: any): any {
if (!data || typeof data !== 'object') {
return data;
}
const transformed = { ...data };
// Remove v3-specific fields
delete transformed.graphqlSchema;
delete transformed.websocketUrl;
delete transformed.batchOperations;
// Transform nested objects
if (transformed.metadata) {
transformed.metadata = transformV3toV2(transformed.metadata);
}
return transformed;
}
function transformV2toV1(data: any): any {
if (!data || typeof data !== 'object') {
return data;
}
const transformed = { ...data };
// Remove v2-specific fields
delete transformed.csrfToken;
delete transformed.sessionInfo;
delete transformed.securityHeaders;
// Simplify error messages for v1
if (transformed.error && typeof transformed.error === 'object') {
transformed.error = transformed.error.message || 'An error occurred';
}
return transformed;
}
// Get API version information
export function getAPIVersionInfo(version?: string): APIVersion | APIVersion[] {
if (version) {
return API_VERSIONS.get(version) || {
version,
deprecated: false,
changes: ['Unknown version']
};
}
return Array.from(API_VERSIONS.values());
}
// Version negotiation helper
export function negotiateVersion(
acceptedVersions: string[],
requiredFeatures?: string[]
): string {
// Find best matching version
for (const version of acceptedVersions) {
if (API_VERSIONS.has(version)) {
const versionInfo = API_VERSIONS.get(version)!;
// Check if version supports required features
if (requiredFeatures) {
const supportsAllFeatures = requiredFeatures.every(feature =>
versionInfo.changes.some(change =>
change.toLowerCase().includes(feature.toLowerCase())
)
);
if (supportsAllFeatures) {
return version;
}
} else {
return version;
}
}
}
return DEFAULT_VERSION;
}
// Generate version documentation
export function generateVersionDocs(): {
current: string;
default: string;
minimum: string;
versions: APIVersion[];
endpoints: Array<{
path: string;
supportedVersions: string[];
}>;
} {
const endpoints = Array.from(versionedEndpoints.entries()).map(([path, endpoint]) => ({
path,
supportedVersions: Array.from(endpoint.versions.keys())
}));
return {
current: CURRENT_VERSION,
default: DEFAULT_VERSION,
minimum: MINIMUM_SUPPORTED_VERSION,
versions: Array.from(API_VERSIONS.values()),
endpoints
};
}
// Export constants for use in other modules
export {
CURRENT_VERSION,
DEFAULT_VERSION,
MINIMUM_SUPPORTED_VERSION
};