Skip to content

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:

  1. Environment Inflexibility: Can't tune per environment
  2. Testing Difficulty: Can't override for fast tests
  3. 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.