All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m56s
219 lines
7.3 KiB
TypeScript
219 lines
7.3 KiB
TypeScript
// src/middleware/apiVersion.middleware.ts
|
|
/**
|
|
* @file API version detection middleware implementing ADR-008 Phase 2.
|
|
*
|
|
* Extracts API version from the request URL, validates it against supported versions,
|
|
* attaches version information to the request object, and handles unsupported versions.
|
|
*
|
|
* @see docs/architecture/api-versioning-infrastructure.md
|
|
* @see docs/adr/0008-api-versioning-strategy.md
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // In versioned router factory (versioned.ts):
|
|
* import { detectApiVersion } from '../middleware/apiVersion.middleware';
|
|
*
|
|
* const router = Router({ mergeParams: true });
|
|
* router.use(detectApiVersion);
|
|
* ```
|
|
*/
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import {
|
|
ApiVersion,
|
|
SUPPORTED_VERSIONS,
|
|
DEFAULT_VERSION,
|
|
isValidApiVersion,
|
|
getVersionDeprecation,
|
|
} from '../config/apiVersions';
|
|
import { sendError } from '../utils/apiResponse';
|
|
import { createScopedLogger } from '../services/logger.server';
|
|
|
|
// --- Module-level Logger ---
|
|
|
|
/**
|
|
* Module-scoped logger for API version middleware.
|
|
* Used for logging version detection events outside of request context.
|
|
*/
|
|
const moduleLogger = createScopedLogger('apiVersion-middleware');
|
|
|
|
// --- Error Codes ---
|
|
|
|
/**
|
|
* Error code for unsupported API version requests.
|
|
* This is specific to the versioning system and not part of the general ErrorCode enum.
|
|
*/
|
|
export const VERSION_ERROR_CODES = {
|
|
UNSUPPORTED_VERSION: 'UNSUPPORTED_VERSION',
|
|
} as const;
|
|
|
|
// --- Middleware Functions ---
|
|
|
|
/**
|
|
* Extracts the API version from the URL path parameter and attaches it to the request.
|
|
*
|
|
* This middleware expects to be used with a router that has a :version parameter
|
|
* (e.g., mounted at `/api/:version`). It validates the version against the list
|
|
* of supported versions and returns a 404 error for unsupported versions.
|
|
*
|
|
* For valid versions, it:
|
|
* - Sets `req.apiVersion` to the detected version
|
|
* - Sets `req.versionDeprecation` with deprecation info if the version is deprecated
|
|
*
|
|
* @param req - Express request object (expects `req.params.version`)
|
|
* @param res - Express response object
|
|
* @param next - Express next function
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Route setup:
|
|
* app.use('/api/:version', detectApiVersion, versionedRouter);
|
|
*
|
|
* // Request to /api/v1/users:
|
|
* // req.params.version = 'v1'
|
|
* // req.apiVersion = 'v1'
|
|
*
|
|
* // Request to /api/v99/users:
|
|
* // Returns 404 with UNSUPPORTED_VERSION error
|
|
* ```
|
|
*/
|
|
export function detectApiVersion(req: Request, res: Response, next: NextFunction): void {
|
|
// Get the request-scoped logger if available, otherwise use module logger
|
|
const log = req.log?.child({ middleware: 'detectApiVersion' }) ?? moduleLogger;
|
|
|
|
// Extract version from URL params (expects router mounted with :version param)
|
|
const versionParam = req.params?.version;
|
|
|
|
// If no version parameter found, this middleware was likely applied incorrectly.
|
|
// Default to the default version and continue (allows for fallback behavior).
|
|
if (!versionParam) {
|
|
log.debug('No version parameter found in request, using default version');
|
|
req.apiVersion = DEFAULT_VERSION;
|
|
req.versionDeprecation = getVersionDeprecation(DEFAULT_VERSION);
|
|
return next();
|
|
}
|
|
|
|
// Validate the version parameter
|
|
if (isValidApiVersion(versionParam)) {
|
|
// Valid version - attach to request
|
|
req.apiVersion = versionParam;
|
|
req.versionDeprecation = getVersionDeprecation(versionParam);
|
|
|
|
log.debug({ apiVersion: versionParam }, 'API version detected from URL');
|
|
return next();
|
|
}
|
|
|
|
// Invalid version - log warning and return 404
|
|
log.warn(
|
|
{
|
|
attemptedVersion: versionParam,
|
|
supportedVersions: SUPPORTED_VERSIONS,
|
|
path: req.path,
|
|
method: req.method,
|
|
ip: req.ip,
|
|
},
|
|
'Invalid API version requested',
|
|
);
|
|
|
|
// Return 404 with UNSUPPORTED_VERSION error code
|
|
// Using 404 because the versioned endpoint does not exist
|
|
sendError(
|
|
res,
|
|
VERSION_ERROR_CODES.UNSUPPORTED_VERSION,
|
|
`API version '${versionParam}' is not supported. Supported versions: ${SUPPORTED_VERSIONS.join(', ')}`,
|
|
404,
|
|
{
|
|
requestedVersion: versionParam,
|
|
supportedVersions: [...SUPPORTED_VERSIONS],
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Extracts the API version from the URL path pattern and attaches it to the request.
|
|
*
|
|
* Unlike `detectApiVersion`, this middleware parses the version from the URL path
|
|
* directly using a regex pattern. This is useful when the middleware needs to run
|
|
* before or independently of parameterized routing.
|
|
*
|
|
* Pattern matched: `/v{number}/...` at the beginning of the path
|
|
* (e.g., `/v1/users`, `/v2/flyers/123`)
|
|
*
|
|
* If the version is valid, sets `req.apiVersion` and `req.versionDeprecation`.
|
|
* If the version is invalid or not present, defaults to `DEFAULT_VERSION`.
|
|
*
|
|
* This middleware does NOT return errors for invalid versions - it's designed for
|
|
* cases where version detection is informational rather than authoritative.
|
|
*
|
|
* @param req - Express request object
|
|
* @param _res - Express response object (unused)
|
|
* @param next - Express next function
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Applied early in middleware chain:
|
|
* app.use('/api', extractApiVersionFromPath, apiRouter);
|
|
*
|
|
* // For path /api/v1/users:
|
|
* // req.path = '/v1/users' (relative to /api mount point)
|
|
* // req.apiVersion = 'v1'
|
|
* ```
|
|
*/
|
|
export function extractApiVersionFromPath(req: Request, _res: Response, next: NextFunction): void {
|
|
// Get the request-scoped logger if available, otherwise use module logger
|
|
const log = req.log?.child({ middleware: 'extractApiVersionFromPath' }) ?? moduleLogger;
|
|
|
|
// Extract version from URL path using regex: /v{number}/
|
|
// The path is relative to the router's mount point
|
|
const pathMatch = req.path.match(/^\/v(\d+)\//);
|
|
|
|
if (pathMatch) {
|
|
const versionString = `v${pathMatch[1]}` as string;
|
|
|
|
if (isValidApiVersion(versionString)) {
|
|
req.apiVersion = versionString;
|
|
req.versionDeprecation = getVersionDeprecation(versionString);
|
|
log.debug({ apiVersion: versionString }, 'API version extracted from path');
|
|
return next();
|
|
}
|
|
|
|
// Version number in path but not in supported list - log and use default
|
|
log.warn(
|
|
{
|
|
attemptedVersion: versionString,
|
|
supportedVersions: SUPPORTED_VERSIONS,
|
|
path: req.path,
|
|
},
|
|
'Unsupported API version in path, falling back to default',
|
|
);
|
|
}
|
|
|
|
// No version detected or invalid - use default
|
|
req.apiVersion = DEFAULT_VERSION;
|
|
req.versionDeprecation = getVersionDeprecation(DEFAULT_VERSION);
|
|
log.debug({ apiVersion: DEFAULT_VERSION }, 'Using default API version');
|
|
return next();
|
|
}
|
|
|
|
/**
|
|
* Type guard to check if a request has a valid API version attached.
|
|
*
|
|
* @param req - Express request object
|
|
* @returns True if req.apiVersion is set to a valid ApiVersion
|
|
*/
|
|
export function hasApiVersion(req: Request): req is Request & { apiVersion: ApiVersion } {
|
|
return req.apiVersion !== undefined && isValidApiVersion(req.apiVersion);
|
|
}
|
|
|
|
/**
|
|
* Gets the API version from a request, with a fallback to the default version.
|
|
*
|
|
* @param req - Express request object
|
|
* @returns The API version from the request, or DEFAULT_VERSION if not set
|
|
*/
|
|
export function getRequestApiVersion(req: Request): ApiVersion {
|
|
if (req.apiVersion && isValidApiVersion(req.apiVersion)) {
|
|
return req.apiVersion;
|
|
}
|
|
return DEFAULT_VERSION;
|
|
}
|