Files
flyer-crawler.projectium.com/src/middleware/deprecation.middleware.ts
Torben Sorensen f10c6c0cd6
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m56s
Complete ADR-008 Phase 2
2026-01-27 11:06:09 -08:00

219 lines
7.7 KiB
TypeScript

// src/middleware/deprecation.middleware.ts
/**
* @file Deprecation Headers Middleware - RFC 8594 Compliant
* Implements ADR-008 Phase 2: API Versioning Infrastructure.
*
* This middleware adds standard deprecation headers to API responses when
* a deprecated API version is being accessed. It follows:
* - RFC 8594: The "Sunset" HTTP Header Field
* - draft-ietf-httpapi-deprecation-header: The "Deprecation" HTTP Header Field
* - RFC 8288: Web Linking (for successor-version relation)
*
* Headers added for deprecated versions:
* - `Deprecation: true` - Indicates the endpoint is deprecated
* - `Sunset: <ISO 8601 date>` - When the endpoint will be removed
* - `Link: </api/vX>; rel="successor-version"` - URL to the replacement version
* - `X-API-Deprecation-Notice: <message>` - Human-readable deprecation message
*
* Always added (for all versions):
* - `X-API-Version: <version>` - The API version being accessed
*
* @see docs/architecture/api-versioning-infrastructure.md
* @see https://datatracker.ietf.org/doc/html/rfc8594
*/
import { Request, Response, NextFunction } from 'express';
import { ApiVersion, VERSION_CONFIGS, getVersionDeprecation } from '../config/apiVersions';
import { createScopedLogger } from '../services/logger.server';
// Create a module-scoped logger for deprecation tracking
const deprecationLogger = createScopedLogger('deprecation-middleware');
/**
* HTTP header names for deprecation signaling.
* Using constants to ensure consistency and prevent typos.
*/
export const DEPRECATION_HEADERS = {
/** RFC draft-ietf-httpapi-deprecation-header: Indicates deprecation status */
DEPRECATION: 'Deprecation',
/** RFC 8594: ISO 8601 date when the endpoint will be removed */
SUNSET: 'Sunset',
/** RFC 8288: Link to successor version with rel="successor-version" */
LINK: 'Link',
/** Custom header: Human-readable deprecation notice */
DEPRECATION_NOTICE: 'X-API-Deprecation-Notice',
/** Custom header: Current API version being accessed */
API_VERSION: 'X-API-Version',
} as const;
/**
* Creates middleware that adds RFC 8594 compliant deprecation headers
* to responses when a deprecated API version is accessed.
*
* This is a middleware factory function that takes a version parameter
* and returns the configured middleware function. This pattern allows
* different version routers to have their own deprecation configuration.
*
* @param version - The API version this middleware is handling
* @returns Express middleware function that adds appropriate headers
*
* @example
* ```typescript
* // In a versioned router factory:
* const v1Router = Router();
* v1Router.use(addDeprecationHeaders('v1'));
*
* // When v1 is deprecated, responses will include:
* // Deprecation: true
* // Sunset: 2027-01-01T00:00:00Z
* // Link: </api/v2>; rel="successor-version"
* // X-API-Deprecation-Notice: API v1 is deprecated...
* // X-API-Version: v1
* ```
*/
export function addDeprecationHeaders(version: ApiVersion) {
// Pre-fetch configuration at middleware creation time for efficiency.
// This avoids repeated lookups on every request.
const config = VERSION_CONFIGS[version];
const deprecationInfo = getVersionDeprecation(version);
return function deprecationHeadersMiddleware(
req: Request,
res: Response,
next: NextFunction,
): void {
// Always set the API version header for transparency and debugging.
// This helps clients know which version they're using, especially
// useful when default version routing is in effect.
res.set(DEPRECATION_HEADERS.API_VERSION, version);
// Only add deprecation headers if this version is actually deprecated.
// Active versions should not have any deprecation headers.
if (config.status === 'deprecated') {
// RFC draft-ietf-httpapi-deprecation-header: Set to "true" to indicate deprecation
res.set(DEPRECATION_HEADERS.DEPRECATION, 'true');
// RFC 8594: Sunset header with ISO 8601 date indicating removal date
if (config.sunsetDate) {
res.set(DEPRECATION_HEADERS.SUNSET, config.sunsetDate);
}
// RFC 8288: Link header with successor-version relation
// This tells clients where to migrate to
if (config.successorVersion) {
res.set(
DEPRECATION_HEADERS.LINK,
`</api/${config.successorVersion}>; rel="successor-version"`,
);
}
// Custom header: Human-readable message for developers
// This provides context that may not be obvious from the standard headers
if (deprecationInfo.message) {
res.set(DEPRECATION_HEADERS.DEPRECATION_NOTICE, deprecationInfo.message);
}
// Attach deprecation info to the request for use in route handlers.
// This allows handlers to implement version-specific behavior or logging.
req.versionDeprecation = deprecationInfo;
// Log deprecation access at debug level to avoid log spam.
// This provides visibility into deprecated API usage without overwhelming logs.
// Use debug level because high-traffic APIs could generate significant volume.
// Production monitoring should use the access logs or metrics aggregation
// to track deprecation usage patterns.
deprecationLogger.debug(
{
apiVersion: version,
method: req.method,
path: req.path,
sunsetDate: config.sunsetDate,
successorVersion: config.successorVersion,
userAgent: req.get('User-Agent'),
// Include request ID if available from the request logger
requestId: (req.log as { bindings?: () => { request_id?: string } })?.bindings?.()
?.request_id,
},
'Deprecated API version accessed',
);
}
next();
};
}
/**
* Standalone middleware for adding deprecation headers based on
* the `apiVersion` property already set on the request.
*
* This middleware should be used after the version extraction middleware
* has set `req.apiVersion`. It provides a more flexible approach when
* the version is determined dynamically rather than statically.
*
* @example
* ```typescript
* // After version extraction middleware:
* router.use(extractApiVersion);
* router.use(addDeprecationHeadersFromRequest);
* ```
*/
export function addDeprecationHeadersFromRequest(
req: Request,
res: Response,
next: NextFunction,
): void {
const version = req.apiVersion;
// If no version is set on the request, skip deprecation handling.
// This should not happen if the version extraction middleware ran first,
// but we handle it gracefully for safety.
if (!version) {
next();
return;
}
const config = VERSION_CONFIGS[version];
const deprecationInfo = getVersionDeprecation(version);
// Always set the API version header
res.set(DEPRECATION_HEADERS.API_VERSION, version);
// Add deprecation headers if version is deprecated
if (config.status === 'deprecated') {
res.set(DEPRECATION_HEADERS.DEPRECATION, 'true');
if (config.sunsetDate) {
res.set(DEPRECATION_HEADERS.SUNSET, config.sunsetDate);
}
if (config.successorVersion) {
res.set(
DEPRECATION_HEADERS.LINK,
`</api/${config.successorVersion}>; rel="successor-version"`,
);
}
if (deprecationInfo.message) {
res.set(DEPRECATION_HEADERS.DEPRECATION_NOTICE, deprecationInfo.message);
}
req.versionDeprecation = deprecationInfo;
deprecationLogger.debug(
{
apiVersion: version,
method: req.method,
path: req.path,
sunsetDate: config.sunsetDate,
successorVersion: config.successorVersion,
userAgent: req.get('User-Agent'),
requestId: (req.log as { bindings?: () => { request_id?: string } })?.bindings?.()
?.request_id,
},
'Deprecated API version accessed',
);
}
next();
}