AI Prompt: Forma3D.Connect — Phase 5p: API Versioning Headers¶
Purpose: This prompt instructs an AI to implement API versioning response headers for better client compatibility
Estimated Effort: 3-4 hours
Prerequisites: Phase 5o completed (Connection Pool)
Output: API version headers in all responses with deprecation support
Status: 🟡 PENDING
🎯 Mission¶
You are continuing development of Forma3D.Connect, building on the Phase 5o foundation. Your task is to implement Phase 5p: API Versioning Headers — specifically addressing TD-014 (Missing API Versioning Headers) from the technical debt register.
Why This Matters:
While URL versioning exists (/api/v1/), clients lack important metadata:
- No Version Confirmation: Clients can't verify which API version they're using
- No Deprecation Notices: No way to signal upcoming breaking changes
- Hard to Debug: Support can't easily identify API version in logs
- No Sunset Dates: Clients aren't informed of end-of-life schedules
Phase 5p delivers:
X-API-Versionheader on all responsesX-API-Deprecatedheader for deprecated endpointsSunsetheader for EOL datesX-API-Min-Versionfor client compatibility checks
📋 Context: Technical Debt Item¶
TD-014: Missing API Versioning Headers¶
| Attribute | Value |
|---|---|
| Type | Architecture Debt |
| Priority | Low-Medium |
| Location | API controllers |
| Interest Rate | Low-Medium |
| Principal (Effort) | 3-4 hours |
🛠️ Implementation Phases¶
Phase 1: Create API Version Configuration (30 minutes)¶
Priority: Critical | Impact: High | Dependencies: None
1. Add Version Constants¶
Create apps/api/src/versioning/api-version.constants.ts:
/**
* API Version Configuration
* Update these when releasing new API versions
*/
export const API_VERSION = {
/** Current API version (semver) */
current: '1.0.0',
/** Minimum supported client version */
minSupported: '1.0.0',
/** API version in URL path */
urlVersion: 'v1',
/** Date when v1 will be deprecated (null if not planned) */
deprecationDate: null as Date | null,
/** Date when v1 will be sunset/removed (null if not planned) */
sunsetDate: null as Date | null,
};
/**
* Standard API response headers
*/
export const API_HEADERS = {
VERSION: 'X-API-Version',
MIN_VERSION: 'X-API-Min-Version',
DEPRECATED: 'X-API-Deprecated',
SUNSET: 'Sunset',
DEPRECATION: 'Deprecation',
} as const;
2. Add to Configuration Service (Optional)¶
If versions should be configurable per environment:
// apps/api/src/config/config.service.ts
get apiVersion(): string {
return this.configService.get<string>('API_VERSION', '1.0.0');
}
Phase 2: Create Versioning Middleware (1 hour)¶
Priority: High | Impact: High | Dependencies: Phase 1
1. Create Version Headers Interceptor¶
Create apps/api/src/versioning/api-version.interceptor.ts:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Response } from 'express';
import { API_VERSION, API_HEADERS } from './api-version.constants';
@Injectable()
export class ApiVersionInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const response = context.switchToHttp().getResponse<Response>();
// Set version headers before response
this.setVersionHeaders(response);
return next.handle().pipe(
tap(() => {
// Headers already set, nothing more to do
}),
);
}
private setVersionHeaders(response: Response): void {
if (response.headersSent) {
return;
}
// Always include current version
response.setHeader(API_HEADERS.VERSION, API_VERSION.current);
response.setHeader(API_HEADERS.MIN_VERSION, API_VERSION.minSupported);
// Add deprecation headers if applicable
if (API_VERSION.deprecationDate) {
response.setHeader(API_HEADERS.DEPRECATED, 'true');
response.setHeader(
API_HEADERS.DEPRECATION,
API_VERSION.deprecationDate.toISOString(),
);
} else {
response.setHeader(API_HEADERS.DEPRECATED, 'false');
}
// Add sunset header if applicable
if (API_VERSION.sunsetDate) {
// RFC 8594 format
response.setHeader(
API_HEADERS.SUNSET,
API_VERSION.sunsetDate.toUTCString(),
);
}
}
}
2. Register Interceptor Globally¶
Update apps/api/src/app.module.ts:
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ApiVersionInterceptor } from './versioning/api-version.interceptor';
@Module({
// ...
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ApiVersionInterceptor,
},
// ... other providers
],
})
export class AppModule {}
Phase 3: Create Deprecation Decorator (1 hour)¶
Priority: Medium | Impact: Medium | Dependencies: Phase 2
1. Create Deprecated Endpoint Decorator¶
Create apps/api/src/versioning/deprecated.decorator.ts:
import { SetMetadata, applyDecorators } from '@nestjs/common';
import { ApiHeader } from '@nestjs/swagger';
export const DEPRECATED_KEY = 'api:deprecated';
export interface DeprecationInfo {
/** Date when endpoint will be removed */
sunsetDate?: Date;
/** Alternative endpoint to use */
alternative?: string;
/** Additional deprecation message */
message?: string;
}
/**
* Mark an endpoint as deprecated
* Adds deprecation headers and Swagger documentation
*
* @example
* @Deprecated({
* sunsetDate: new Date('2026-06-01'),
* alternative: '/api/v2/orders',
* message: 'Use v2 API for improved performance'
* })
* @Get('legacy-orders')
* getLegacyOrders() { ... }
*/
export function Deprecated(info: DeprecationInfo = {}) {
return applyDecorators(
SetMetadata(DEPRECATED_KEY, info),
ApiHeader({
name: 'X-API-Deprecated',
description: 'This endpoint is deprecated',
example: 'true',
}),
ApiHeader({
name: 'Sunset',
description: 'Date when this endpoint will be removed',
example: 'Sat, 01 Jun 2026 00:00:00 GMT',
required: false,
}),
);
}
2. Create Deprecation Interceptor¶
Create apps/api/src/versioning/deprecated.interceptor.ts:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { Response } from 'express';
import { DEPRECATED_KEY, DeprecationInfo } from './deprecated.decorator';
import { API_HEADERS } from './api-version.constants';
@Injectable()
export class DeprecatedInterceptor implements NestInterceptor {
private readonly logger = new Logger(DeprecatedInterceptor.name);
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const deprecationInfo = this.reflector.get<DeprecationInfo>(
DEPRECATED_KEY,
context.getHandler(),
);
if (deprecationInfo) {
const response = context.switchToHttp().getResponse<Response>();
const request = context.switchToHttp().getRequest();
this.setDeprecationHeaders(response, deprecationInfo);
this.logDeprecatedUsage(request, deprecationInfo);
}
return next.handle();
}
private setDeprecationHeaders(
response: Response,
info: DeprecationInfo,
): void {
if (response.headersSent) {
return;
}
response.setHeader(API_HEADERS.DEPRECATED, 'true');
if (info.sunsetDate) {
response.setHeader(API_HEADERS.SUNSET, info.sunsetDate.toUTCString());
}
if (info.alternative) {
response.setHeader('Link', `<${info.alternative}>; rel="successor-version"`);
}
if (info.message) {
response.setHeader('X-API-Deprecation-Notice', info.message);
}
}
private logDeprecatedUsage(
request: { path: string; ip: string },
info: DeprecationInfo,
): void {
this.logger.warn({
message: 'Deprecated endpoint accessed',
path: request.path,
clientIp: request.ip,
sunsetDate: info.sunsetDate?.toISOString(),
alternative: info.alternative,
});
}
}
3. Register Deprecation Interceptor¶
Update apps/api/src/app.module.ts:
import { DeprecatedInterceptor } from './versioning/deprecated.interceptor';
@Module({
// ...
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ApiVersionInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: DeprecatedInterceptor,
},
],
})
export class AppModule {}
Phase 4: Add Swagger Documentation (30 minutes)¶
Priority: Medium | Impact: Low | Dependencies: Phase 2
1. Update Swagger Configuration¶
Update apps/api/src/main.ts:
import { API_VERSION } from './versioning/api-version.constants';
// In bootstrap function:
const config = new DocumentBuilder()
.setTitle('Forma3D.Connect API')
.setDescription('Order fulfillment automation API')
.setVersion(API_VERSION.current)
.addApiKey({ type: 'apiKey', name: 'X-API-Key', in: 'header' }, 'api-key')
.build();
2. Add Global Response Headers to Swagger¶
const document = SwaggerModule.createDocument(app, config, {
extraModels: [],
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
});
// Add global response headers documentation
Object.values(document.paths).forEach((path) => {
Object.values(path).forEach((operation: any) => {
if (operation.responses) {
Object.values(operation.responses).forEach((response: any) => {
response.headers = {
...response.headers,
'X-API-Version': {
description: 'Current API version',
schema: { type: 'string', example: '1.0.0' },
},
'X-API-Deprecated': {
description: 'Whether this endpoint is deprecated',
schema: { type: 'string', example: 'false' },
},
};
});
}
});
});
Phase 5: Testing (30 minutes)¶
Priority: High | Impact: Medium | Dependencies: Phase 2
1. Create Unit Tests¶
Create apps/api/src/versioning/api-version.interceptor.spec.ts:
import { Test } from '@nestjs/testing';
import { ApiVersionInterceptor } from './api-version.interceptor';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { of } from 'rxjs';
import { API_VERSION } from './api-version.constants';
describe('ApiVersionInterceptor', () => {
let interceptor: ApiVersionInterceptor;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [ApiVersionInterceptor],
}).compile();
interceptor = module.get(ApiVersionInterceptor);
});
it('should add version headers to response', (done) => {
const mockResponse = {
headersSent: false,
setHeader: jest.fn(),
};
const mockContext = {
switchToHttp: () => ({
getResponse: () => mockResponse,
}),
} as unknown as ExecutionContext;
const mockHandler: CallHandler = {
handle: () => of({}),
};
interceptor.intercept(mockContext, mockHandler).subscribe(() => {
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'X-API-Version',
API_VERSION.current,
);
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'X-API-Deprecated',
'false',
);
done();
});
});
});
📁 Files to Create/Modify¶
New Files¶
apps/api/src/versioning/api-version.constants.ts
apps/api/src/versioning/api-version.interceptor.ts
apps/api/src/versioning/deprecated.decorator.ts
apps/api/src/versioning/deprecated.interceptor.ts
apps/api/src/versioning/api-version.interceptor.spec.ts
apps/api/src/versioning/index.ts
Modified Files¶
apps/api/src/app.module.ts
apps/api/src/main.ts (Swagger config)
✅ Validation Checklist¶
- API version constants defined
- ApiVersionInterceptor adds headers to all responses
- Deprecated decorator available for endpoints
- Swagger documentation updated with version
- Tests verify header presence
-
pnpm nx build apipasses -
pnpm nx test apipasses
Final Verification¶
# Build passes
pnpm nx build api
# Tests pass
pnpm nx test api
# Start API and check headers
pnpm nx serve api
# Verify headers in response
curl -i http://localhost:3000/api/v1/orders
# Should include:
# X-API-Version: 1.0.0
# X-API-Min-Version: 1.0.0
# X-API-Deprecated: false
📝 Usage Example¶
// For deprecated endpoints:
@Controller('api/v1/legacy')
export class LegacyController {
@Get('orders')
@Deprecated({
sunsetDate: new Date('2026-12-31'),
alternative: '/api/v2/orders',
message: 'Migrate to v2 API for improved filtering'
})
getLegacyOrders() {
// Will add deprecation headers automatically
}
}
END OF PROMPT
This prompt resolves TD-014 from the technical debt register by implementing API versioning headers.