diff --git a/backend/src/docs/VERSIONING.md b/backend/src/docs/VERSIONING.md new file mode 100644 index 0000000..bbd03e6 --- /dev/null +++ b/backend/src/docs/VERSIONING.md @@ -0,0 +1,357 @@ +# LuminaryTrade API Versioning Guide + +## Overview + +The LuminaryTrade API uses **URL-based versioning**. Every versioned endpoint +is prefixed with `/vN/` where `N` is the version number. + +``` +https://api.luminarytrade.com/v1/transactions +https://api.luminarytrade.com/v2/transactions +``` + +Unversioned paths (e.g. `/health`, `/metrics`) are not subject to version +lifecycle management. + +--- + +## Current Versions + +| Version | Status | Released | Sunset | Notes | +|---------|------------|------------|------------|------------------------------| +| v0 | ๐Ÿšซ SUNSET | 2023-01-01 | 2024-07-01 | Permanently removed | +| v1 | โœ… STABLE | 2024-01-01 | โ€” | Default. Recommended for all new integrations | +| v2 | ๐Ÿงช BETA | 2025-01-01 | โ€” | Early access. Breaking changes may occur | + +### Default version + +Requests without a version prefix are routed to **v1**. + +--- + +## Lifecycle + +``` +BETA โ†’ STABLE โ†’ DEPRECATED โ†’ SUNSET +``` + +| Phase | What happens | +|--------------|------------------------------------------------------------------------------| +| **BETA** | Available but unstable. Response includes `X-Api-Version-Status: beta`. | +| **STABLE** | Production-ready. No extra headers. | +| **DEPRECATED** | Returns warning headers (see below). Works normally for 3+ months. | +| **SUNSET** | Returns `410 Gone`. No response body. Migration required. | + +### Minimum grace period + +There is a mandatory **3-month** minimum between the deprecation announcement +and the sunset date. This gives consumers time to migrate. + +--- + +## Response Headers + +### Deprecated version headers + +Every response from a deprecated version includes: + +``` +Sunset: Mon, 01 Jul 2024 00:00:00 GMT +Deprecation: Mon, 01 Jan 2024 00:00:00 GMT +X-Deprecated: true +X-Sunset-Date: 2024-07-01 +X-Api-Version-Status: deprecated +X-Deprecation-Info: https://docs.luminarytrade.com/api/versioning#migration +Link: ; rel="deprecation" +``` + +`Sunset` and `Deprecation` follow [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594). + +### Sunset response (410 Gone) + +```json +{ + "success": false, + "error": { + "code": "API_VERSION_SUNSET", + "message": "API version 0 has been permanently removed as of 2024-07-01.", + "sunsetDate": "2024-07-01", + "migrationGuide": "https://docs.luminarytrade.com/api/versioning", + "currentStableVersion": "1", + "timestamp": "2025-01-15T10:00:00.000Z", + "path": "/v0/transactions" + } +} +``` + +### Beta version headers + +``` +X-Api-Version-Status: beta +X-Beta-Warning: This API version is in beta. Breaking changes may occur before stable release. +``` + +--- + +## v0 โ†’ v1 Migration + +### Error response shape + +**v0:** +```json +{ "error": "Invalid credentials", "code": 401 } +``` + +**v1:** +```json +{ + "success": false, + "error": { + "code": "AUTH_001", + "message": "Invalid credentials", + "timestamp": "2025-01-15T10:00:00.000Z", + "path": "/v1/auth/login" + } +} +``` + +**Action:** Update error handling to read `error.code` and `error.message` +instead of the top-level `error` string. + +### Pagination + +**v0** used offset-based pagination: +``` +GET /v0/transactions?page=2&pageSize=20 +``` + +**v1** uses cursor-based pagination: +``` +GET /v1/transactions?cursor=eyJpZCI6MTIzfQ&limit=20 +``` + +Response includes: +```json +{ + "data": [...], + "pagination": { + "nextCursor": "eyJpZCI6MTQzfQ", + "hasMore": true, + "limit": 20 + } +} +``` + +**Action:** Replace `page`/`pageSize` params with `cursor`/`limit`. +Store the `nextCursor` from each response and pass it as `cursor` in the +next request. + +### Authentication + +**v0** used API keys in the query string: +``` +GET /v0/transactions?api_key=sk_live_... +``` + +**v1** uses Bearer tokens in the Authorization header: +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... +``` + +**Action:** Move authentication to the `Authorization` header. Never send +credentials in query strings. + +--- + +## v1 โ†’ v2 Migration + +v2 is currently in **BETA**. The following changes are planned and may still +evolve before v2 reaches STABLE. + +### Renamed fields + +| v1 field | v2 field | Affected endpoints | +|-----------------|-------------|---------------------------------------| +| `walletAddress` | `address` | All user and transaction endpoints | +| `createdAt` | `created` | All resource endpoints | +| `updatedAt` | `updated` | All resource endpoints | +| `userId` | `user.id` | Transaction and audit endpoints | + +**v1 response:** +```json +{ + "walletAddress": "GABC...XYZ", + "createdAt": "2025-01-15T10:00:00.000Z", + "userId": "usr_123" +} +``` + +**v2 response:** +```json +{ + "address": "GABC...XYZ", + "created": "2025-01-15T10:00:00.000Z", + "user": { "id": "usr_123" } +} +``` + +### Removed fields + +The following fields are returned in v1 but **omitted** in v2: + +| Field | v1 | v2 | Replacement | +|-----------------|-------|--------|----------------------------------| +| `legacyId` | โœ… | โŒ | Use `id` (UUID) | +| `rawPayload` | โœ… | โŒ | Use `data` (parsed object) | +| `statusCode` | โœ… | โŒ | Use `status` (string enum) | + +### New fields in v2 + +These fields are **optional** and only appear in v2 responses: + +- `meta.requestId` โ€” traces the request through the system +- `meta.processingTimeMs` โ€” server-side processing time +- `links.self` / `links.related` โ€” HAL-style resource links + +### Streaming responses + +v2 supports Server-Sent Events for long-running operations: + +``` +GET /v2/transactions/stream +Accept: text/event-stream +``` + +v1 uses polling: +``` +POST /v1/transactions +GET /v1/transactions/:id/status +``` + +### GraphQL + +v2 exposes a GraphQL endpoint alongside REST: + +``` +POST /v2/graphql +Content-Type: application/json + +{ "query": "{ transactions { id address amount } }" } +``` + +--- + +## Targeting a version in client SDKs + +### TypeScript / JavaScript + +```typescript +import { LuminaryClient } from '@luminarytrade/sdk'; + +// Target v1 (stable, recommended) +const client = new LuminaryClient({ + baseUrl: 'https://api.luminarytrade.com', + version: 'v1', + apiKey: process.env.LUMINARY_API_KEY, +}); + +// Target v2 (beta) +const betaClient = new LuminaryClient({ + baseUrl: 'https://api.luminarytrade.com', + version: 'v2', + apiKey: process.env.LUMINARY_API_KEY, +}); + +// Explicit per-request version override +const tx = await client.transactions.get('txn_123', { version: 'v2' }); +``` + +### cURL + +```bash +# v1 +curl -H "Authorization: Bearer $TOKEN" \ + https://api.luminarytrade.com/v1/transactions + +# v2 (beta) +curl -H "Authorization: Bearer $TOKEN" \ + https://api.luminarytrade.com/v2/transactions + +# Check deprecation headers +curl -I -H "Authorization: Bearer $TOKEN" \ + https://api.luminarytrade.com/v0/transactions +# โ†’ HTTP/1.1 410 Gone +``` + +### Python + +```python +import requests + +BASE = "https://api.luminarytrade.com" +HEADERS = {"Authorization": f"Bearer {api_key}"} + +# v1 +r = requests.get(f"{BASE}/v1/transactions", headers=HEADERS) + +# v2 +r = requests.get(f"{BASE}/v2/transactions", headers=HEADERS) + +# Check for deprecation warnings +if r.headers.get("X-Deprecated"): + sunset = r.headers.get("X-Sunset-Date") + print(f"Warning: this API version is deprecated and will be removed on {sunset}") +``` + +--- + +## Annotating controllers (backend developers) + +```typescript +import { Controller, Get } from '@nestjs/common'; +import { ApiVersion } from '../versioning/api-version.decorator'; + +// All routes in this controller are under /v1/transactions +@ApiVersion('1') +@Controller('transactions') +export class TransactionV1Controller { + @Get() + findAll() { + // Returns v1 shape: walletAddress, createdAt + } +} + +// All routes in this controller are under /v2/transactions +@ApiVersion('2') +@Controller('transactions') +export class TransactionV2Controller { + @Get() + findAll() { + // Returns v2 shape: address, created + } +} +``` + +--- + +## Adding a new version + +1. Add an entry to `src/versioning/version.constants.ts` +2. Create a new controller file `src/*/controllers/*.v3.controller.ts` +3. Annotate it with `@ApiVersion('3')` +4. Register the controller in the relevant feature module +5. Document breaking changes in this file under a new migration section + +## Deprecating a version + +1. Change `status` to `'DEPRECATED'` in `version.constants.ts` +2. Set `deprecatedAt` to today's date +3. Set `sunsetAt` to at least 3 months from today +4. Publish a deprecation announcement via `POST /v1/announcements` +5. Update this file with the migration guide + +## Removing a version (after sunset) + +1. Change `status` to `'SUNSET'` in `version.constants.ts` +2. The middleware will return 410 Gone automatically +3. Remove the sunset controller code in a follow-up PR (optional โ€” 410 runs first) \ No newline at end of file diff --git a/backend/src/logging/middleware/correlation-id.middleware.ts b/backend/src/logging/middleware/correlation-id.middleware.ts index a54f447..b1e7d5b 100644 --- a/backend/src/logging/middleware/correlation-id.middleware.ts +++ b/backend/src/logging/middleware/correlation-id.middleware.ts @@ -5,6 +5,7 @@ import { traceContextStorage } from '../../common/async-storage'; @Injectable() export class CorrelationIdMiddleware implements NestMiddleware { + name = 'CorrelationIdMiddleware'; use(req: Request, res: Response, next: NextFunction) { const correlationId = (req.headers['x-correlation-id'] as string) || uuidv4(); const userId = (req as any).user?.id || (req.headers['x-user-id'] as string); diff --git a/backend/src/main.ts b/backend/src/main.ts index 3bb0047..72f3784 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,5 +1,5 @@ import { NestFactory } from '@nestjs/core'; -import { ValidationPipe } from '@nestjs/common'; +import { ValidationPipe, VersioningType } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import cookieParser from 'cookie-parser'; import helmet from 'helmet'; @@ -8,7 +8,6 @@ import { RateLimitGuard } from './rate-limiting/guards/rate-limit.guard'; import { SystemLoadMiddleware } from './rate-limiting/middleware/system-load.middleware'; import { TracingInterceptor } from './tracing/interceptors/tracing.interceptor'; import { TracingMiddleware } from './tracing/middleware/tracing.middleware'; -import { GlobalExceptionFilter } from './common/filters/global-exception.filter'; import { StartupService } from './startup/services/startup.service'; import { MiddlewarePipeline } from './middleware-pipeline/pipeline'; import { wrap } from './middleware-pipeline/adapters/express-wrapper'; @@ -20,6 +19,7 @@ import { RateLimitMiddleware } from './middleware-pipeline/middlewares/rate-limi import { CorsMiddleware } from './middleware-pipeline/middlewares/cors.middleware'; import { ResponseTransformInterceptor } from './middleware-pipeline/interceptors/response-transform.interceptor'; import { CorrelationIdMiddleware } from './logging/middleware/correlation-id.middleware'; +import { DEFAULT_API_VERSION } from './versioning/version.constants'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -28,14 +28,25 @@ async function bootstrap() { app.getHttpAdapter().getInstance().set('trust proxy', 1); - // Note: CORS, cookie-parser, and helmet are registered via the middleware pipeline below + // โ”€โ”€ API Versioning โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Enable URI-based versioning: /v1/*, /v2/*, etc. + // Controllers opt in with @ApiVersion('1') or @Version('1'). + // Requests without a version prefix are routed to DEFAULT_API_VERSION. + // Deprecation lifecycle (410 Gone, warning headers) is handled by + // VersionCheckMiddleware registered in VersioningModule. + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: DEFAULT_API_VERSION, + }); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, })); - app.useGlobalFilters(new GlobalExceptionFilter()); + // Note: GlobalExceptionFilter is registered via APP_FILTER in AppModule + // (added in the i18n issue) so NestJS can inject I18nService into it. + // Do NOT call app.useGlobalFilters(new GlobalExceptionFilter()) here. // Apply tracing interceptor globally const tracingInterceptor = app.get(TracingInterceptor); @@ -75,13 +86,11 @@ async function bootstrap() { app.enableShutdownHooks(); const port = process.env.PORT || 3000; - - // Wait for startup service to complete before listening + console.log('๐Ÿ”„ Waiting for startup sequence to complete...'); - - // Check if startup is complete before starting the server - const maxWaitTime = 60000; // 60 seconds max wait time - const checkInterval = 1000; // Check every second + + const maxWaitTime = 60000; + const checkInterval = 1000; let waitTime = 0; while (!startupService.isReady() && waitTime < maxWaitTime) { @@ -95,18 +104,21 @@ async function bootstrap() { } await app.listen(port); - + console.log(`๐Ÿš€ ChenAIKit Backend running on port ${port}`); console.log(`๐Ÿ“ก Submitter service running on http://localhost:${port}`); console.log(`๐Ÿ›ก๏ธ Rate limiting enabled with adaptive strategies`); console.log(`๐Ÿ” Distributed tracing enabled - Jaeger UI: http://localhost:16686`); + console.log(`๐ŸŒ API versioning enabled โ€” default version: v${DEFAULT_API_VERSION}`); + console.log(` Stable: http://localhost:${port}/v1/`); + console.log(` Beta: http://localhost:${port}/v2/`); + console.log(` Sunset: http://localhost:${port}/v0/ โ†’ 410 Gone`); console.log(`๐Ÿฅ Health endpoints available:`); - console.log(` - Startup: http://localhost:${port}/health/startup`); + console.log(` - Startup: http://localhost:${port}/health/startup`); console.log(` - Readiness: http://localhost:${port}/health/readiness`); - console.log(` - Liveness: http://localhost:${port}/health/liveness`); - console.log(` - Full Health: http://localhost:${port}/health`); - - // Log startup report + console.log(` - Liveness: http://localhost:${port}/health/liveness`); + console.log(` - Full: http://localhost:${port}/health`); + const report = startupService.getStartupReport(); if (report) { console.log(`โœ… Startup completed in ${report.totalDuration}ms`); @@ -117,4 +129,4 @@ async function bootstrap() { bootstrap().catch(error => { console.error('โŒ Failed to start application:', error); process.exit(1); -}); +}); \ No newline at end of file diff --git a/backend/src/versioning/api-version.decorator.ts b/backend/src/versioning/api-version.decorator.ts new file mode 100644 index 0000000..a7bf9df --- /dev/null +++ b/backend/src/versioning/api-version.decorator.ts @@ -0,0 +1,67 @@ +import { Version, applyDecorators } from '@nestjs/common'; +import { ApiHeader, ApiOperation } from '@nestjs/swagger'; +import { API_VERSIONS, VersionStatus } from './version.constants'; + +export function ApiVersion(version: keyof typeof API_VERSIONS) { + const config = API_VERSIONS[version]; + const isDeprecated = config?.status === 'DEPRECATED' || config?.status === 'SUNSET'; + + const decorators = [ + // NestJS built-in โ€” wires URL prefix /v{version}/ + Version(String(version)), + ]; + + // Add Swagger documentation if available + if (config) { + // Mark deprecated endpoints in Swagger UI + if (isDeprecated && config.sunsetAt) { + decorators.push( + ApiHeader({ + name: 'X-Deprecated', + description: `This API version is deprecated and will be removed on ${config.sunsetAt}`, + required: false, + }), + ApiHeader({ + name: 'X-Sunset-Date', + description: `Sunset date: ${config.sunsetAt}`, + required: false, + }), + ); + } + } + + return applyDecorators(...decorators); +} + +export function VersionedEndpoint(options: { + version: keyof typeof API_VERSIONS; + summary?: string; + notes?: string; +}) { + const config = API_VERSIONS[options.version]; + const status: VersionStatus = config?.status ?? 'STABLE'; + + const statusBadge: Record = { + BETA: 'BETA', + STABLE: 'STABLE', + DEPRECATED: 'DEPRECATED', + SUNSET: 'SUNSET', + }; + + const summary = [ + statusBadge[status], + options.summary ?? '', + ].filter(Boolean).join(' โ€” '); + + const description = [ + options.notes, + config?.status === 'DEPRECATED' || config?.status === 'SUNSET' + ? `**Removal date:** ${config?.sunsetAt ?? 'TBD'}. See [migration guide](../docs/VERSIONING.md).` + : undefined, + ].filter(Boolean).join('\n\n'); + + return applyDecorators( + Version(String(options.version)), + ApiOperation({ summary, description }), + ); +} \ No newline at end of file diff --git a/backend/src/versioning/version-check.middleware.ts b/backend/src/versioning/version-check.middleware.ts new file mode 100644 index 0000000..7701864 --- /dev/null +++ b/backend/src/versioning/version-check.middleware.ts @@ -0,0 +1,88 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { + API_VERSIONS, + DEFAULT_API_VERSION, + isVersionDeprecated, + isVersionSunset, +} from './version.constants'; + +@Injectable() +export class VersionCheckMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction): void { + const version = this.resolveVersion(req.path); + + // Always add which version was resolved + res.setHeader('X-Api-Version', version); + + // โ”€โ”€ SUNSET โ†’ 410 Gone โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if (isVersionSunset(version)) { + const config = API_VERSIONS[version]; + const sunsetDate = config?.sunsetAt ?? 'unknown'; + + res.status(410).json({ + success: false, + error: { + code: 'API_VERSION_SUNSET', + message: `API version ${version} has been permanently removed as of ${sunsetDate}.`, + sunsetDate, + migrationGuide: 'https://docs.luminarytrade.com/api/versioning', + currentStableVersion: DEFAULT_API_VERSION, + timestamp: new Date().toISOString(), + path: req.path, + }, + }); + return; + } + + // โ”€โ”€ DEPRECATED โ†’ warn headers, continue โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if (isVersionDeprecated(version)) { + const config = API_VERSIONS[version]; + + if (config?.sunsetAt) { + const sunsetRfc = new Date(config.sunsetAt).toUTCString(); + const deprecatedRfc = config.deprecatedAt + ? new Date(config.deprecatedAt).toUTCString() + : sunsetRfc; + + // Standard RFC 8594 headers + res.setHeader('Sunset', sunsetRfc); + res.setHeader('Deprecation', deprecatedRfc); + + // Informational headers for clients that read non-standard headers + res.setHeader('X-Deprecated', 'true'); + res.setHeader('X-Sunset-Date', config.sunsetAt); + res.setHeader('X-Api-Version-Status', 'deprecated'); + res.setHeader( + 'X-Deprecation-Info', + 'https://docs.luminarytrade.com/api/versioning#migration', + ); + res.setHeader( + 'Link', + `; rel="deprecation"`, + ); + } + } + + // โ”€โ”€ BETA โ†’ informational header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const config = API_VERSIONS[version]; + if (config?.status === 'BETA') { + res.setHeader('X-Api-Version-Status', 'beta'); + res.setHeader( + 'X-Beta-Warning', + 'This API version is in beta. Breaking changes may occur before stable release.', + ); + } + + next(); + } + + private resolveVersion(path: string): string { + const match = path.match(/^\/v(\d+)\//); + if (match) return match[1]; + const matchNoSlash = path.match(/^\/v(\d+)$/); + if (matchNoSlash) return matchNoSlash[1]; + + return DEFAULT_API_VERSION; + } +} \ No newline at end of file diff --git a/backend/src/versioning/version.constants.ts b/backend/src/versioning/version.constants.ts new file mode 100644 index 0000000..708b479 --- /dev/null +++ b/backend/src/versioning/version.constants.ts @@ -0,0 +1,58 @@ +export type VersionStatus = 'BETA' | 'STABLE' | 'DEPRECATED' | 'SUNSET'; + +export interface ApiVersionConfig { + version: string; + status: VersionStatus; + releasedAt: string; + deprecatedAt?: string; + sunsetAt?: string; + changelog: string; +} + +export const API_VERSIONS: Record = { + '0': { + version: '0', + status: 'SUNSET', + releasedAt: '2023-01-01', + deprecatedAt: '2024-01-01', + sunsetAt: '2024-07-01', + changelog: 'Original release. Superseded by v1 with improved error shapes and pagination.', + }, + '1': { + version: '1', + status: 'STABLE', + releasedAt: '2024-01-01', + changelog: 'Stable release. Standardised error envelope, cursor-based pagination, ' + + 'JWT authentication, Stellar transaction support.', + }, + + '2': { + version: '2', + status: 'BETA', + releasedAt: '2025-01-01', + changelog: 'Beta release. GraphQL endpoints, streaming support, ' + + 'renamed fields (walletAddress โ†’ address), improved pagination.', + }, +} as const; + +export const DEFAULT_API_VERSION = '1'; + +export const SUPPORTED_VERSIONS = Object.values(API_VERSIONS) + .filter((v) => v.status !== 'SUNSET') + .map((v) => v.version); + +export function isVersionSunset(versionKey: string): boolean { + const config = API_VERSIONS[versionKey]; + if (!config) return true; // unknown version = treat as sunset + if (config.status === 'SUNSET') return true; + if (config.sunsetAt && new Date() > new Date(config.sunsetAt)) return true; + return false; +} + +export function isVersionDeprecated(versionKey: string): boolean { + const config = API_VERSIONS[versionKey]; + if (!config) return false; + if (config.status === 'DEPRECATED') return true; + if (config.status === 'SUNSET') return false; + return false; +} \ No newline at end of file diff --git a/backend/src/versioning/versioning.module.ts b/backend/src/versioning/versioning.module.ts new file mode 100644 index 0000000..d1f6e5f --- /dev/null +++ b/backend/src/versioning/versioning.module.ts @@ -0,0 +1,14 @@ +import { Module, MiddlewareConsumer, NestModule, RequestMethod } from '@nestjs/common'; +import { VersionCheckMiddleware } from './version-check.middleware'; + +@Module({ + providers: [VersionCheckMiddleware], + exports: [VersionCheckMiddleware], +}) +export class VersioningModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer + .apply(VersionCheckMiddleware) + .forRoutes({ path: '*', method: RequestMethod.ALL }); + } +} \ No newline at end of file