AI Prompt: Forma3D.Connect — Phase 5k: Configuration Externalization¶
Purpose: This prompt instructs an AI to externalize hardcoded configuration values to environment variables
Estimated Effort: 1-2 days (~8-12 hours)
Prerequisites: Phase 5j completed (Error Types)
Output: All configuration values externalized, validated at startup, documented
Status: 🟡 PENDING
🎯 Mission¶
You are continuing development of Forma3D.Connect, building on the Phase 5j foundation. Your task is to implement Phase 5k: Configuration Externalization — specifically addressing TD-009 (Hardcoded Configuration Values) from the technical debt register.
Why This Matters:
Several configuration values are hardcoded rather than injected, causing:
- Environment Inflexibility: Can't tune per environment
- Testing Difficulty: Can't override for fast tests
- Operational Blindness: Values not visible in config dumps
Phase 5k delivers:
- All configuration in environment variables
- Validated at startup with class-validator
- Documented with defaults and descriptions
- Configurable per environment
📋 Context: Technical Debt Item¶
TD-009: Hardcoded Configuration Values¶
| Attribute | Value |
|---|---|
| Type | Infrastructure Debt |
| Priority | High |
| Location | Multiple services |
| Interest Rate | Medium |
| Principal (Effort) | 1-2 days |
Current Hardcoded Values¶
| Location | Value | Description |
|---|---|---|
retry-queue.service.ts |
maxRetries: 5 |
Max retry attempts |
retry-queue.service.ts |
initialDelayMs: 1000 |
Initial retry delay |
retry-queue.service.ts |
maxDelayMs: 3600000 |
Max retry delay |
simplyprint-api.client.ts |
timeout: 30000 |
API timeout |
webhook-cleanup.service.ts |
24 * 60 * 60 * 1000 |
Webhook TTL |
🛠️ Implementation Phases¶
Phase 1: Create Configuration Module (2 hours)¶
Priority: Critical | Impact: High | Dependencies: None
1. Create Configuration Schema¶
Create apps/api/src/config/configuration.ts:
import { IsNumber, IsString, IsOptional, Min, Max, IsBoolean } from 'class-validator';
import { Transform } from 'class-transformer';
export class EnvironmentVariables {
// === Database ===
@IsString()
DATABASE_URL!: string;
// === Server ===
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1)
@Max(65535)
PORT: number = 3000;
@IsString()
@IsOptional()
NODE_ENV: string = 'development';
// === API Keys ===
@IsString()
API_KEY!: string;
// === Retry Configuration ===
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1)
@Max(10)
RETRY_MAX_ATTEMPTS: number = 5;
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(100)
RETRY_INITIAL_DELAY_MS: number = 1000;
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1000)
RETRY_MAX_DELAY_MS: number = 3600000;
@Transform(({ value }) => parseFloat(value))
@IsNumber()
@Min(1)
@Max(5)
RETRY_BACKOFF_MULTIPLIER: number = 2;
// === SimplyPrint ===
@IsString()
@IsOptional()
SIMPLYPRINT_API_URL?: string;
@IsString()
@IsOptional()
SIMPLYPRINT_API_KEY?: string;
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(5000)
@Max(120000)
SIMPLYPRINT_TIMEOUT_MS: number = 30000;
// === Sendcloud ===
@IsString()
@IsOptional()
SENDCLOUD_API_KEY?: string;
@IsString()
@IsOptional()
SENDCLOUD_API_SECRET?: string;
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(5000)
SENDCLOUD_TIMEOUT_MS: number = 30000;
// === Shopify ===
@IsString()
@IsOptional()
SHOPIFY_WEBHOOK_SECRET?: string;
// === Webhook Cleanup ===
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(3600000) // Min 1 hour
WEBHOOK_TTL_MS: number = 86400000; // 24 hours
@IsString()
WEBHOOK_CLEANUP_CRON: string = '0 * * * *'; // Every hour
// === Observability ===
@IsString()
@IsOptional()
OTEL_EXPORTER_OTLP_ENDPOINT?: string;
@IsString()
@IsOptional()
SENTRY_DSN?: string;
@IsBoolean()
@Transform(({ value }) => value === 'true')
LOG_PRETTY: boolean = false;
@IsString()
LOG_LEVEL: string = 'info';
}
2. Create Configuration Validation¶
Create apps/api/src/config/env.validation.ts:
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { EnvironmentVariables } from './configuration';
export function validate(config: Record<string, unknown>) {
const validatedConfig = plainToInstance(EnvironmentVariables, config, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
const errorMessages = errors.map(error => {
const constraints = Object.values(error.constraints || {});
return `${error.property}: ${constraints.join(', ')}`;
});
throw new Error(`Configuration validation failed:\n${errorMessages.join('\n')}`);
}
return validatedConfig;
}
3. Create Configuration Service¶
Create apps/api/src/config/config.service.ts:
import { Injectable } from '@nestjs/common';
import { ConfigService as NestConfigService } from '@nestjs/config';
export interface RetryConfig {
maxAttempts: number;
initialDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
}
export interface SimplyPrintConfig {
apiUrl: string | undefined;
apiKey: string | undefined;
timeoutMs: number;
}
export interface WebhookConfig {
ttlMs: number;
cleanupCron: string;
}
@Injectable()
export class AppConfigService {
constructor(private configService: NestConfigService) {}
get port(): number {
return this.configService.get<number>('PORT', 3000);
}
get nodeEnv(): string {
return this.configService.get<string>('NODE_ENV', 'development');
}
get isProduction(): boolean {
return this.nodeEnv === 'production';
}
get apiKey(): string {
return this.configService.getOrThrow<string>('API_KEY');
}
get retry(): RetryConfig {
return {
maxAttempts: this.configService.get<number>('RETRY_MAX_ATTEMPTS', 5),
initialDelayMs: this.configService.get<number>('RETRY_INITIAL_DELAY_MS', 1000),
maxDelayMs: this.configService.get<number>('RETRY_MAX_DELAY_MS', 3600000),
backoffMultiplier: this.configService.get<number>('RETRY_BACKOFF_MULTIPLIER', 2),
};
}
get simplyPrint(): SimplyPrintConfig {
return {
apiUrl: this.configService.get<string>('SIMPLYPRINT_API_URL'),
apiKey: this.configService.get<string>('SIMPLYPRINT_API_KEY'),
timeoutMs: this.configService.get<number>('SIMPLYPRINT_TIMEOUT_MS', 30000),
};
}
get webhook(): WebhookConfig {
return {
ttlMs: this.configService.get<number>('WEBHOOK_TTL_MS', 86400000),
cleanupCron: this.configService.get<string>('WEBHOOK_CLEANUP_CRON', '0 * * * *'),
};
}
get databaseUrl(): string {
return this.configService.getOrThrow<string>('DATABASE_URL');
}
}
4. Update App Module¶
Update apps/api/src/app.module.ts:
import { ConfigModule } from '@nestjs/config';
import { validate } from './config/env.validation';
import { AppConfigService } from './config/config.service';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validate,
cache: true,
}),
// ... other imports
],
providers: [
AppConfigService,
// ... other providers
],
exports: [AppConfigService],
})
export class AppModule {}
Phase 2: Update Services (3 hours)¶
Priority: High | Impact: High | Dependencies: Phase 1
1. Update Retry Queue Service¶
Update apps/api/src/retry/retry-queue.service.ts:
import { Injectable } from '@nestjs/common';
import { AppConfigService } from '../config/config.service';
@Injectable()
export class RetryQueueService {
private readonly config: RetryConfig;
constructor(private readonly appConfig: AppConfigService) {
this.config = this.appConfig.retry;
}
calculateDelay(retryCount: number): number {
const delay = this.config.initialDelayMs *
Math.pow(this.config.backoffMultiplier, retryCount);
return Math.min(delay, this.config.maxDelayMs);
}
shouldRetry(retryCount: number): boolean {
return retryCount < this.config.maxAttempts;
}
}
2. Update SimplyPrint Client¶
Update apps/api/src/simplyprint/simplyprint-api.client.ts:
import { Injectable } from '@nestjs/common';
import { AppConfigService } from '../config/config.service';
@Injectable()
export class SimplyPrintApiClient {
private readonly timeout: number;
private readonly apiUrl: string;
constructor(private readonly appConfig: AppConfigService) {
const config = this.appConfig.simplyPrint;
this.timeout = config.timeoutMs;
this.apiUrl = config.apiUrl || 'https://api.simplyprint.io';
}
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(`${this.apiUrl}${endpoint}`, {
...options,
signal: controller.signal,
});
// ... handle response
} finally {
clearTimeout(timeoutId);
}
}
}
3. Update Webhook Cleanup Service¶
Update apps/api/src/shopify/webhook-cleanup.service.ts:
import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { AppConfigService } from '../config/config.service';
@Injectable()
export class WebhookCleanupService {
constructor(
private readonly appConfig: AppConfigService,
private readonly repository: WebhookIdempotencyRepository,
) {}
// Note: Cron decorator doesn't support dynamic values
// For dynamic cron, use SchedulerRegistry
@Cron('0 * * * *')
async cleanupExpiredWebhooks(): Promise<void> {
const ttlMs = this.appConfig.webhook.ttlMs;
const cutoffDate = new Date(Date.now() - ttlMs);
await this.repository.deleteExpired(cutoffDate);
}
}
Phase 3: Create .env.example (1 hour)¶
Priority: High | Impact: Medium | Dependencies: Phase 1
Create apps/api/.env.example:
# ===========================================
# Forma3D.Connect API Configuration
# ===========================================
# === Required ===
DATABASE_URL=postgresql://user:password@localhost:5432/forma3d_connect
API_KEY=your-secure-api-key-here
# === Server ===
PORT=3000
NODE_ENV=development
# === Retry Configuration ===
RETRY_MAX_ATTEMPTS=5
RETRY_INITIAL_DELAY_MS=1000
RETRY_MAX_DELAY_MS=3600000
RETRY_BACKOFF_MULTIPLIER=2
# === SimplyPrint Integration ===
SIMPLYPRINT_API_URL=https://api.simplyprint.io
SIMPLYPRINT_API_KEY=your-simplyprint-api-key
SIMPLYPRINT_TIMEOUT_MS=30000
# === Sendcloud Integration ===
SENDCLOUD_API_KEY=your-sendcloud-key
SENDCLOUD_API_SECRET=your-sendcloud-secret
SENDCLOUD_TIMEOUT_MS=30000
# === Shopify Integration ===
SHOPIFY_WEBHOOK_SECRET=your-shopify-webhook-secret
# === Webhook Cleanup ===
WEBHOOK_TTL_MS=86400000
WEBHOOK_CLEANUP_CRON=0 * * * *
# === Observability ===
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
SENTRY_DSN=
LOG_PRETTY=true
LOG_LEVEL=debug
Phase 4: Documentation Updates (30 minutes)¶
Update docs/04-development/techdebt/technical-debt-register.md:
### ~~TD-009: Hardcoded Configuration Values~~ ✅ RESOLVED
**Type:** Infrastructure Debt
**Status:** ✅ **Resolved in Phase 5k**
**Resolution Date:** 2026-XX-XX
#### Resolution
Externalized all hardcoded configuration to environment variables:
| Configuration | Environment Variable | Default |
|---------------|---------------------|---------|
| Retry max attempts | `RETRY_MAX_ATTEMPTS` | 5 |
| Retry initial delay | `RETRY_INITIAL_DELAY_MS` | 1000 |
| Retry max delay | `RETRY_MAX_DELAY_MS` | 3600000 |
| SimplyPrint timeout | `SIMPLYPRINT_TIMEOUT_MS` | 30000 |
| Webhook TTL | `WEBHOOK_TTL_MS` | 86400000 |
**Files Created:**
- `apps/api/src/config/configuration.ts`
- `apps/api/src/config/env.validation.ts`
- `apps/api/src/config/config.service.ts`
- `apps/api/.env.example`
📁 Files to Create/Modify¶
New Files¶
apps/api/src/config/configuration.ts
apps/api/src/config/env.validation.ts
apps/api/src/config/config.service.ts
apps/api/.env.example
Modified Files¶
apps/api/src/app.module.ts
apps/api/src/retry/retry-queue.service.ts
apps/api/src/simplyprint/simplyprint-api.client.ts
apps/api/src/shopify/webhook-cleanup.service.ts
docs/04-development/techdebt/technical-debt-register.md
✅ Validation Checklist¶
- Configuration schema created with validation
- AppConfigService provides typed access
- All hardcoded values externalized
- Services updated to use config service
- .env.example documents all variables
- Startup fails gracefully on missing required config
- Tests pass with config overrides
Final Verification¶
# Build passes
pnpm nx build api
# Tests pass
pnpm nx test api
# App starts with valid config
pnpm nx serve api
END OF PROMPT
This prompt resolves TD-009 from the technical debt register by externalizing hardcoded configuration values.