13 HTTP-Client und externe APIs

Die Integration externer APIs ist ein wesentlicher Bestandteil moderner Backend-Anwendungen. NestJS bietet robuste Tools und Patterns für die HTTP-Kommunikation mit externen Services. In diesem Kapitel behandeln wir die verschiedenen Ansätze für HTTP-Clients, Fehlerbehandlung, Retry-Mechanismen und Best Practices für die Integration externer APIs.

13.1 HttpModule in NestJS

Das HttpModule von NestJS basiert auf Axios und bietet eine nahtlose Integration für HTTP-Requests. Es unterstützt sowohl Promise-basierte als auch Observable-basierte APIs und integriert sich perfekt in das Dependency Injection System von NestJS.

13.1.1 Grundlegende Konfiguration

13.1.1.1 Installation und Setup

npm install @nestjs/axios axios

13.1.1.2 Module-Konfiguration

import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    HttpModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        timeout: configService.get('HTTP_TIMEOUT', 5000),
        maxRedirects: configService.get('HTTP_MAX_REDIRECTS', 5),
        retries: configService.get('HTTP_RETRIES', 3),
        headers: {
          'User-Agent': 'NestJS-App/1.0',
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [ExternalApiService],
  exports: [ExternalApiService],
})
export class ExternalApiModule {}

13.1.1.3 Basis HTTP-Service

import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { AxiosResponse } from 'axios';
import { Observable, catchError, map, retry, timeout } from 'rxjs';

@Injectable()
export class BaseHttpService {
  private readonly logger = new Logger(BaseHttpService.name);

  constructor(private readonly httpService: HttpService) {}

  protected get<T>(url: string, config?: any): Observable<T> {
    this.logger.debug(`GET ${url}`);
    
    return this.httpService.get<T>(url, config).pipe(
      timeout(10000),
      retry(2),
      map((response: AxiosResponse<T>) => response.data),
      catchError(this.handleError.bind(this)),
    );
  }

  protected post<T>(url: string, data?: any, config?: any): Observable<T> {
    this.logger.debug(`POST ${url}`);
    
    return this.httpService.post<T>(url, data, config).pipe(
      timeout(15000),
      retry(1),
      map((response: AxiosResponse<T>) => response.data),
      catchError(this.handleError.bind(this)),
    );
  }

  protected put<T>(url: string, data?: any, config?: any): Observable<T> {
    this.logger.debug(`PUT ${url}`);
    
    return this.httpService.put<T>(url, data, config).pipe(
      timeout(15000),
      map((response: AxiosResponse<T>) => response.data),
      catchError(this.handleError.bind(this)),
    );
  }

  protected delete<T>(url: string, config?: any): Observable<T> {
    this.logger.debug(`DELETE ${url}`);
    
    return this.httpService.delete<T>(url, config).pipe(
      timeout(10000),
      map((response: AxiosResponse<T>) => response.data),
      catchError(this.handleError.bind(this)),
    );
  }

  private handleError(error: any): Observable<never> {
    this.logger.error('HTTP request failed:', error.message);
    throw error;
  }
}

13.2 Axios-Integration

Axios ist eine mächtige HTTP-Client-Bibliothek, die umfangreiche Konfigurationsmöglichkeiten bietet. NestJS integriert Axios nahtlos über das HttpModule.

13.2.1 Erweiterte Axios-Konfiguration

13.2.1.1 Custom Axios-Instanz

import { Injectable, OnModuleInit } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { AxiosRequestConfig, AxiosResponse } from 'axios';

@Injectable()
export class CustomHttpService implements OnModuleInit {
  constructor(
    private readonly httpService: HttpService,
    private readonly configService: ConfigService,
  ) {}

  onModuleInit() {
    const axios = this.httpService.axiosRef;

    // Request Interceptor
    axios.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        // API Key hinzufügen
        const apiKey = this.configService.get('EXTERNAL_API_KEY');
        if (apiKey) {
          config.headers['Authorization'] = `Bearer ${apiKey}`;
        }

        // Request ID für Tracing
        config.headers['X-Request-ID'] = this.generateRequestId();
        
        // Logging
        console.log(`[${new Date().toISOString()}] ${config.method?.toUpperCase()} ${config.url}`);
        
        return config;
      },
      (error) => {
        console.error('Request interceptor error:', error);
        return Promise.reject(error);
      },
    );

    // Response Interceptor
    axios.interceptors.response.use(
      (response: AxiosResponse) => {
        // Response Time logging
        const requestTime = response.config.metadata?.requestStartTime;
        if (requestTime) {
          const duration = Date.now() - requestTime;
          console.log(`Request completed in ${duration}ms`);
        }

        return response;
      },
      (error) => {
        if (error.response) {
          // Server responded with error status
          console.error(
            `HTTP ${error.response.status}: ${error.response.statusText}`,
            error.response.data,
          );
        } else if (error.request) {
          // Request was made but no response received
          console.error('No response received:', error.message);
        } else {
          // Something else happened
          console.error('Request setup error:', error.message);
        }

        return Promise.reject(error);
      },
    );

    // Request timing
    axios.interceptors.request.use(
      (config) => {
        config.metadata = { requestStartTime: Date.now() };
        return config;
      },
    );
  }

  private generateRequestId(): string {
    return Math.random().toString(36).substring(2, 15);
  }
}

13.2.2 TypeScript-Typen für API-Responses

// DTOs für externe API-Responses
export interface WeatherApiResponse {
  current: {
    temperature: number;
    humidity: number;
    description: string;
    windSpeed: number;
  };
  forecast: Array<{
    date: string;
    maxTemp: number;
    minTemp: number;
    description: string;
  }>;
}

export interface UserApiResponse {
  id: string;
  name: string;
  email: string;
  avatar: string;
  createdAt: string;
}

export interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

// Error Response Types
export interface ApiErrorResponse {
  error: {
    code: string;
    message: string;
    details?: Record<string, any>;
  };
  timestamp: string;
  path: string;
}

13.3 REST-API-Clients erstellen

Das Erstellen strukturierter API-Clients verbessert die Wartbarkeit und Wiederverwendbarkeit des Codes erheblich.

13.3.1 Generischer API-Client

import { Injectable, BadRequestException, InternalServerErrorException } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Observable, throwError, of } from 'rxjs';
import { map, catchError, switchMap, tap } from 'rxjs/operators';
import { AxiosError } from 'axios';

@Injectable()
export class GenericApiClient {
  private readonly baseUrl: string;
  private readonly defaultHeaders: Record<string, string>;

  constructor(
    private readonly httpService: HttpService,
    private readonly configService: ConfigService,
  ) {
    this.baseUrl = this.configService.get<string>('EXTERNAL_API_BASE_URL');
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    };
  }

  protected request<T>(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE',
    endpoint: string,
    data?: any,
    headers?: Record<string, string>,
  ): Observable<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const config = {
      headers: { ...this.defaultHeaders, ...headers },
    };

    let request$: Observable<any>;

    switch (method) {
      case 'GET':
        request$ = this.httpService.get(url, config);
        break;
      case 'POST':
        request$ = this.httpService.post(url, data, config);
        break;
      case 'PUT':
        request$ = this.httpService.put(url, data, config);
        break;
      case 'DELETE':
        request$ = this.httpService.delete(url, config);
        break;
    }

    return request$.pipe(
      map(response => response.data),
      catchError(this.handleApiError.bind(this)),
    );
  }

  private handleApiError(error: AxiosError): Observable<never> {
    if (error.response) {
      const status = error.response.status;
      const message = error.response.data?.message || error.message;

      switch (status) {
        case 400:
          throw new BadRequestException(`Bad Request: ${message}`);
        case 401:
          throw new BadRequestException(`Unauthorized: ${message}`);
        case 403:
          throw new BadRequestException(`Forbidden: ${message}`);
        case 404:
          throw new BadRequestException(`Not Found: ${message}`);
        case 429:
          throw new BadRequestException(`Rate Limited: ${message}`);
        case 500:
        default:
          throw new InternalServerErrorException(`External API Error: ${message}`);
      }
    }

    throw new InternalServerErrorException(`Network Error: ${error.message}`);
  }
}

13.3.2 Spezifische API-Service-Implementierungen

13.3.2.1 Weather API Service

@Injectable()
export class WeatherApiService extends GenericApiClient {
  constructor(
    httpService: HttpService,
    configService: ConfigService,
  ) {
    super(httpService, configService);
  }

  getCurrentWeather(city: string): Observable<WeatherApiResponse> {
    const apiKey = this.configService.get<string>('WEATHER_API_KEY');
    
    return this.request<WeatherApiResponse>(
      'GET',
      `/weather/current?city=${encodeURIComponent(city)}&key=${apiKey}`,
    ).pipe(
      tap(data => console.log(`Weather data received for ${city}`)),
      catchError(error => {
        console.error(`Failed to get weather for ${city}:`, error);
        return throwError(() => error);
      }),
    );
  }

  getWeatherForecast(
    city: string,
    days: number = 5,
  ): Observable<WeatherApiResponse> {
    const apiKey = this.configService.get<string>('WEATHER_API_KEY');
    
    return this.request<WeatherApiResponse>(
      'GET',
      `/weather/forecast?city=${encodeURIComponent(city)}&days=${days}&key=${apiKey}`,
    );
  }

  getWeatherByCoordinates(
    lat: number,
    lon: number,
  ): Observable<WeatherApiResponse> {
    const apiKey = this.configService.get<string>('WEATHER_API_KEY');
    
    return this.request<WeatherApiResponse>(
      'GET',
      `/weather/coordinates?lat=${lat}&lon=${lon}&key=${apiKey}`,
    );
  }
}

13.3.2.2 Users API Service

@Injectable()
export class UsersApiService extends GenericApiClient {
  constructor(
    httpService: HttpService,
    configService: ConfigService,
  ) {
    super(httpService, configService);
  }

  getUsers(
    page: number = 1,
    limit: number = 10,
  ): Observable<PaginatedResponse<UserApiResponse>> {
    return this.request<PaginatedResponse<UserApiResponse>>(
      'GET',
      `/users?page=${page}&limit=${limit}`,
    );
  }

  getUserById(id: string): Observable<UserApiResponse> {
    return this.request<UserApiResponse>('GET', `/users/${id}`);
  }

  createUser(userData: Partial<UserApiResponse>): Observable<UserApiResponse> {
    return this.request<UserApiResponse>('POST', '/users', userData);
  }

  updateUser(
    id: string,
    userData: Partial<UserApiResponse>,
  ): Observable<UserApiResponse> {
    return this.request<UserApiResponse>('PUT', `/users/${id}`, userData);
  }

  deleteUser(id: string): Observable<void> {
    return this.request<void>('DELETE', `/users/${id}`);
  }

  searchUsers(query: string): Observable<UserApiResponse[]> {
    return this.request<UserApiResponse[]>(
      'GET',
      `/users/search?q=${encodeURIComponent(query)}`,
    );
  }
}

13.4 Retry-Mechanismen und Fehlerbehandlung

Robuste Fehlerbehandlung und Retry-Mechanismen sind essentiell für die Zuverlässigkeit von API-Integrationen.

13.4.1 Erweiterte Retry-Strategien

import { Injectable } from '@nestjs/common';
import { Observable, throwError, timer, of } from 'rxjs';
import { retryWhen, mergeMap, finalize, tap } from 'rxjs/operators';

@Injectable()
export class RetryableHttpService {
  private readonly maxRetries = 3;
  private readonly retryDelay = 1000; // Base delay in ms

  constructor(private readonly httpService: HttpService) {}

  requestWithExponentialBackoff<T>(
    requestFn: () => Observable<T>,
    maxRetries: number = this.maxRetries,
  ): Observable<T> {
    return requestFn().pipe(
      retryWhen(errors =>
        errors.pipe(
          mergeMap((error, retryAttempt) => {
            // Nicht bei bestimmten Fehlern retry
            if (this.shouldNotRetry(error)) {
              return throwError(() => error);
            }

            // Max retries erreicht
            if (retryAttempt >= maxRetries) {
              return throwError(() => new Error(`Max retries (${maxRetries}) exceeded`));
            }

            // Exponential backoff: 1s, 2s, 4s, 8s...
            const delay = this.retryDelay * Math.pow(2, retryAttempt);
            
            console.log(`Retry attempt ${retryAttempt + 1} after ${delay}ms`);
            
            return timer(delay);
          }),
        ),
      ),
      finalize(() => console.log('Request completed')),
    );
  }

  requestWithLinearBackoff<T>(
    requestFn: () => Observable<T>,
    maxRetries: number = this.maxRetries,
  ): Observable<T> {
    return requestFn().pipe(
      retryWhen(errors =>
        errors.pipe(
          mergeMap((error, retryAttempt) => {
            if (this.shouldNotRetry(error) || retryAttempt >= maxRetries) {
              return throwError(() => error);
            }

            // Linear backoff: 1s, 2s, 3s, 4s...
            const delay = this.retryDelay * (retryAttempt + 1);
            
            return timer(delay);
          }),
        ),
      ),
    );
  }

  requestWithJitterBackoff<T>(
    requestFn: () => Observable<T>,
    maxRetries: number = this.maxRetries,
  ): Observable<T> {
    return requestFn().pipe(
      retryWhen(errors =>
        errors.pipe(
          mergeMap((error, retryAttempt) => {
            if (this.shouldNotRetry(error) || retryAttempt >= maxRetries) {
              return throwError(() => error);
            }

            // Jitter: Zufällige Komponente zur Vermeidung von Thundering Herd
            const baseDelay = this.retryDelay * Math.pow(2, retryAttempt);
            const jitter = Math.random() * 1000; // 0-1000ms random
            const delay = baseDelay + jitter;
            
            return timer(delay);
          }),
        ),
      ),
    );
  }

  private shouldNotRetry(error: any): boolean {
    // Nicht retry bei Client-Fehlern (4xx außer 429)
    if (error.response && error.response.status >= 400 && error.response.status < 500) {
      return error.response.status !== 429; // Retry bei Rate Limiting
    }
    
    return false;
  }
}

13.4.2 Circuit Breaker Pattern

@Injectable()
export class CircuitBreakerService {
  private failures = 0;
  private lastFailureTime = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  
  private readonly failureThreshold = 5;
  private readonly recoveryTimeout = 30000; // 30 seconds
  private readonly monitoringPeriod = 60000; // 1 minute

  constructor(private readonly logger: Logger) {}

  execute<T>(operation: () => Observable<T>): Observable<T> {
    if (this.state === 'OPEN') {
      if (this.shouldAttemptRecovery()) {
        this.state = 'HALF_OPEN';
        this.logger.log('Circuit breaker: Attempting recovery (HALF_OPEN)');
      } else {
        return throwError(() => new Error('Circuit breaker is OPEN'));
      }
    }

    return operation().pipe(
      tap(() => this.onSuccess()),
      catchError(error => {
        this.onError();
        return throwError(() => error);
      }),
    );
  }

  private onSuccess(): void {
    this.failures = 0;
    if (this.state === 'HALF_OPEN') {
      this.state = 'CLOSED';
      this.logger.log('Circuit breaker: Recovered (CLOSED)');
    }
  }

  private onError(): void {
    this.failures++;
    this.lastFailureTime = Date.now();

    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.logger.warn(`Circuit breaker: Opened due to ${this.failures} failures`);
    }
  }

  private shouldAttemptRecovery(): boolean {
    return Date.now() - this.lastFailureTime >= this.recoveryTimeout;
  }

  getState(): { state: string; failures: number; lastFailureTime: number } {
    return {
      state: this.state,
      failures: this.failures,
      lastFailureTime: this.lastFailureTime,
    };
  }
}

13.4.3 Comprehensive Error Handler

@Injectable()
export class ApiErrorHandler {
  private readonly logger = new Logger(ApiErrorHandler.name);

  handleError(error: AxiosError, context?: string): Observable<never> {
    const errorInfo = this.extractErrorInfo(error);
    
    this.logError(errorInfo, context);
    
    // Custom Exception basierend auf Error Type
    switch (errorInfo.type) {
      case 'NETWORK_ERROR':
        throw new ServiceUnavailableException(
          'External service is currently unavailable',
        );
      
      case 'TIMEOUT_ERROR':
        throw new RequestTimeoutException(
          'Request to external service timed out',
        );
      
      case 'VALIDATION_ERROR':
        throw new BadRequestException(errorInfo.message);
      
      case 'AUTHENTICATION_ERROR':
        throw new UnauthorizedException(errorInfo.message);
      
      case 'AUTHORIZATION_ERROR':
        throw new ForbiddenException(errorInfo.message);
      
      case 'NOT_FOUND_ERROR':
        throw new NotFoundException(errorInfo.message);
      
      case 'RATE_LIMIT_ERROR':
        throw new TooManyRequestsException(errorInfo.message);
      
      case 'SERVER_ERROR':
      default:
        throw new InternalServerErrorException(
          'External service error occurred',
        );
    }
  }

  private extractErrorInfo(error: AxiosError): {
    type: string;
    message: string;
    status?: number;
    details?: any;
  } {
    if (!error.response) {
      // Network or timeout error
      if (error.code === 'ECONNABORTED') {
        return { type: 'TIMEOUT_ERROR', message: 'Request timeout' };
      }
      return { type: 'NETWORK_ERROR', message: error.message };
    }

    const status = error.response.status;
    const data = error.response.data;

    switch (status) {
      case 400:
        return {
          type: 'VALIDATION_ERROR',
          message: data?.message || 'Bad request',
          status,
          details: data,
        };
      case 401:
        return {
          type: 'AUTHENTICATION_ERROR',
          message: 'Authentication failed',
          status,
        };
      case 403:
        return {
          type: 'AUTHORIZATION_ERROR',
          message: 'Access forbidden',
          status,
        };
      case 404:
        return {
          type: 'NOT_FOUND_ERROR',
          message: 'Resource not found',
          status,
        };
      case 429:
        return {
          type: 'RATE_LIMIT_ERROR',
          message: 'Rate limit exceeded',
          status,
        };
      case 500:
      case 502:
      case 503:
      case 504:
        return {
          type: 'SERVER_ERROR',
          message: 'External server error',
          status,
        };
      default:
        return {
          type: 'UNKNOWN_ERROR',
          message: 'Unknown error occurred',
          status,
        };
    }
  }

  private logError(errorInfo: any, context?: string): void {
    const logContext = context ? `[${context}]` : '';
    
    this.logger.error(
      `${logContext} API Error: ${errorInfo.type} - ${errorInfo.message}`,
      errorInfo.details ? JSON.stringify(errorInfo.details) : undefined,
    );
  }
}

13.5 HTTP-Interceptors für externe Calls

HTTP-Interceptors ermöglichen es, Request und Response-Processing zentral zu verwalten und Cross-Cutting-Concerns zu implementieren.

13.5.1 Custom HTTP-Interceptor

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';

@Injectable()
export class HttpLoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const startTime = Date.now();
    
    console.log(`[HTTP] ${request.method} ${request.url} - Start`);

    return next.handle().pipe(
      tap(data => {
        const duration = Date.now() - startTime;
        console.log(`[HTTP] ${request.method} ${request.url} - Success (${duration}ms)`);
      }),
      catchError(error => {
        const duration = Date.now() - startTime;
        console.error(`[HTTP] ${request.method} ${request.url} - Error (${duration}ms):`, error.message);
        throw error;
      }),
    );
  }
}

13.5.2 Rate Limiting Interceptor

@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
  private requestCounts = new Map<string, { count: number; resetTime: number }>();
  private readonly limit = 100; // Requests per window
  private readonly windowMs = 60000; // 1 minute

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const clientIp = request.ip || 'unknown';
    
    if (!this.checkRateLimit(clientIp)) {
      throw new TooManyRequestsException('Rate limit exceeded');
    }

    return next.handle();
  }

  private checkRateLimit(clientIp: string): boolean {
    const now = Date.now();
    const clientData = this.requestCounts.get(clientIp);

    if (!clientData || now > clientData.resetTime) {
      // New window
      this.requestCounts.set(clientIp, {
        count: 1,
        resetTime: now + this.windowMs,
      });
      return true;
    }

    if (clientData.count >= this.limit) {
      return false;
    }

    clientData.count++;
    return true;
  }
}

13.5.3 Authentication Interceptor

@Injectable()
export class AuthenticationInterceptor implements NestInterceptor {
  constructor(
    private readonly configService: ConfigService,
    private readonly jwtService: JwtService,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    
    // API Token für externe Services hinzufügen
    const externalApiToken = this.configService.get('EXTERNAL_API_TOKEN');
    if (externalApiToken && this.isExternalApiCall(request)) {
      request.headers['Authorization'] = `Bearer ${externalApiToken}`;
    }

    // Request Correlation ID
    if (!request.headers['x-correlation-id']) {
      request.headers['x-correlation-id'] = this.generateCorrelationId();
    }

    return next.handle().pipe(
      tap(response => {
        // Response-Processing
        if (response && response.headers) {
          console.log('Response headers:', response.headers);
        }
      }),
    );
  }

  private isExternalApiCall(request: any): boolean {
    return request.url.includes('/api/external/') || 
           request.headers['x-external-api'] === 'true';
  }

  private generateCorrelationId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}

13.6 Caching von HTTP-Responses

Caching reduziert die Latenz und die Last auf externe APIs erheblich.

13.6.1 In-Memory HTTP-Cache

import { Injectable, Logger } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap, share } from 'rxjs/operators';

interface CacheEntry<T> {
  data: T;
  timestamp: number;
  ttl: number;
}

@Injectable()
export class HttpCacheService {
  private readonly cache = new Map<string, CacheEntry<any>>();
  private readonly ongoingRequests = new Map<string, Observable<any>>();
  private readonly logger = new Logger(HttpCacheService.name);

  get<T>(
    key: string,
    factory: () => Observable<T>,
    ttlMs: number = 300000, // 5 minutes default
  ): Observable<T> {
    // Check cache first
    const cached = this.getFromCache<T>(key);
    if (cached) {
      this.logger.debug(`Cache hit for key: ${key}`);
      return of(cached);
    }

    // Check if request is already ongoing
    const ongoing = this.ongoingRequests.get(key);
    if (ongoing) {
      this.logger.debug(`Using ongoing request for key: ${key}`);
      return ongoing as Observable<T>;
    }

    // Make new request
    this.logger.debug(`Cache miss for key: ${key}, making new request`);
    
    const request$ = factory().pipe(
      tap(data => {
        this.setCache(key, data, ttlMs);
        this.ongoingRequests.delete(key);
      }),
      share(), // Share the observable with multiple subscribers
    );

    this.ongoingRequests.set(key, request$);
    return request$;
  }

  private getFromCache<T>(key: string): T | null {
    const entry = this.cache.get(key);
    
    if (!entry) {
      return null;
    }

    const now = Date.now();
    if (now - entry.timestamp > entry.ttl) {
      this.cache.delete(key);
      return null;
    }

    return entry.data as T;
  }

  private setCache<T>(key: string, data: T, ttlMs: number): void {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl: ttlMs,
    });

    this.logger.debug(`Cached data for key: ${key}, TTL: ${ttlMs}ms`);
  }

  invalidate(key: string): void {
    this.cache.delete(key);
    this.ongoingRequests.delete(key);
    this.logger.debug(`Invalidated cache for key: ${key}`);
  }

  invalidatePattern(pattern: string): void {
    const regex = new RegExp(pattern);
    
    for (const key of this.cache.keys()) {
      if (regex.test(key)) {
        this.invalidate(key);
      }
    }
  }

  clear(): void {
    this.cache.clear();
    this.ongoingRequests.clear();
    this.logger.debug('Cleared all cache entries');
  }

  getStats(): { size: number; keys: string[] } {
    return {
      size: this.cache.size,
      keys: Array.from(this.cache.keys()),
    };
  }
}

13.6.2 Cache-Decorator

import { SetMetadata } from '@nestjs/common';

export const CACHE_TTL_KEY = 'cache_ttl';
export const CACHE_KEY_GENERATOR = 'cache_key_generator';

export function Cacheable(
  ttlMs: number = 300000,
  keyGenerator?: (args: any[]) => string,
) {
  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
    SetMetadata(CACHE_TTL_KEY, ttlMs)(target, propertyName, descriptor);
    
    if (keyGenerator) {
      SetMetadata(CACHE_KEY_GENERATOR, keyGenerator)(target, propertyName, descriptor);
    }

    return descriptor;
  };
}

// Usage
@Injectable()
export class WeatherService {
  constructor(
    private readonly weatherApi: WeatherApiService,
    private readonly cacheService: HttpCacheService,
  ) {}

  @Cacheable(600000) // Cache for 10 minutes
  getWeatherForCity(city: string): Observable<WeatherApiResponse> {
    const cacheKey = `weather:${city}`;
    
    return this.cacheService.get(
      cacheKey,
      () => this.weatherApi.getCurrentWeather(city),
      600000,
    );
  }

  @Cacheable(1800000, (args) => `weather:coordinates:${args[0]}:${args[1]}`) // 30 minutes
  getWeatherByCoordinates(lat: number, lon: number): Observable<WeatherApiResponse> {
    const cacheKey = `weather:coordinates:${lat}:${lon}`;
    
    return this.cacheService.get(
      cacheKey,
      () => this.weatherApi.getWeatherByCoordinates(lat, lon),
      1800000,
    );
  }
}

13.6.3 Redis-basiertes Caching

import { Injectable, Inject } from '@nestjs/common';
import { Redis } from 'ioredis';
import { Observable, from, of } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';

@Injectable()
export class RedisHttpCacheService {
  constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}

  get<T>(
    key: string,
    factory: () => Observable<T>,
    ttlSeconds: number = 300,
  ): Observable<T> {
    return from(this.redis.get(key)).pipe(
      switchMap(cached => {
        if (cached) {
          try {
            const data = JSON.parse(cached) as T;
            console.log(`Redis cache hit for key: ${key}`);
            return of(data);
          } catch (error) {
            console.error('Error parsing cached data:', error);
            // Remove invalid cache entry
            this.redis.del(key);
          }
        }

        console.log(`Redis cache miss for key: ${key}`);
        return factory().pipe(
          tap(data => {
            const serialized = JSON.stringify(data);
            this.redis.setex(key, ttlSeconds, serialized);
            console.log(`Cached data in Redis for key: ${key}`);
          }),
        );
      }),
    );
  }

  async invalidate(key: string): Promise<void> {
    await this.redis.del(key);
    console.log(`Invalidated Redis cache for key: ${key}`);
  }

  async invalidatePattern(pattern: string): Promise<void> {
    const keys = await this.redis.keys(pattern);
    if (keys.length > 0) {
      await this.redis.del(...keys);
      console.log(`Invalidated ${keys.length} Redis cache entries matching pattern: ${pattern}`);
    }
  }

  async clear(): Promise<void> {
    await this.redis.flushdb();
    console.log('Cleared Redis cache');
  }

  async getStats(): Promise<{ size: number; memory: string }> {
    const dbsize = await this.redis.dbsize();
    const memory = await this.redis.memory('usage');
    
    return {
      size: dbsize,
      memory: `${(memory / 1024 / 1024).toFixed(2)} MB`,
    };
  }
}

13.7 Best Practices für externe API-Integration

13.7.1 API-Client Factory

@Injectable()
export class ApiClientFactory {
  private readonly clients = new Map<string, any>();

  constructor(
    private readonly httpService: HttpService,
    private readonly configService: ConfigService,
    private readonly cacheService: HttpCacheService,
  ) {}

  createClient<T>(config: {
    name: string;
    baseUrl: string;
    timeout?: number;
    retries?: number;
    cache?: boolean;
    authentication?: {
      type: 'bearer' | 'api-key' | 'basic';
      credentials: any;
    };
  }): T {
    if (this.clients.has(config.name)) {
      return this.clients.get(config.name);
    }

    const client = new GenericApiClient(
      this.httpService,
      this.configService,
    );

    // Configure client based on config
    this.configureClient(client, config);

    this.clients.set(config.name, client);
    return client as T;
  }

  private configureClient(client: any, config: any): void {
    // Implementation specific to your needs
    if (config.authentication) {
      this.setupAuthentication(client, config.authentication);
    }

    if (config.cache) {
      this.setupCaching(client);
    }

    // Add more configuration as needed
  }

  private setupAuthentication(client: any, auth: any): void {
    // Implement authentication setup
  }

  private setupCaching(client: any): void {
    // Implement caching setup
  }
}

13.7.2 Environment-spezifische Konfiguration

// config/api.config.ts
export interface ApiConfig {
  weather: {
    baseUrl: string;
    apiKey: string;
    timeout: number;
    retries: number;
  };
  users: {
    baseUrl: string;
    timeout: number;
    rateLimit: number;
  };
  payments: {
    baseUrl: string;
    apiKey: string;
    timeout: number;
    webhookSecret: string;
  };
}

export const apiConfig = (): { apis: ApiConfig } => ({
  apis: {
    weather: {
      baseUrl: process.env.WEATHER_API_URL || 'https://api.weather.com',
      apiKey: process.env.WEATHER_API_KEY || '',
      timeout: parseInt(process.env.WEATHER_API_TIMEOUT) || 5000,
      retries: parseInt(process.env.WEATHER_API_RETRIES) || 3,
    },
    users: {
      baseUrl: process.env.USERS_API_URL || 'https://api.users.com',
      timeout: parseInt(process.env.USERS_API_TIMEOUT) || 5000,
      rateLimit: parseInt(process.env.USERS_API_RATE_LIMIT) || 100,
    },
    payments: {
      baseUrl: process.env.PAYMENTS_API_URL || 'https://api.payments.com',
      apiKey: process.env.PAYMENTS_API_KEY || '',
      timeout: parseInt(process.env.PAYMENTS_API_TIMEOUT) || 10000,
      webhookSecret: process.env.PAYMENTS_WEBHOOK_SECRET || '',
    },
  },
});

13.7.3 Health Check für externe APIs

@Injectable()
export class ExternalApiHealthIndicator extends HealthIndicator {
  constructor(
    private readonly weatherApi: WeatherApiService,
    private readonly usersApi: UsersApiService,
  ) {
    super();
  }

  async checkWeatherApi(): Promise<HealthIndicatorResult> {
    try {
      await this.weatherApi.getCurrentWeather('London').toPromise();
      return this.getStatus('weather-api', true);
    } catch (error) {
      return this.getStatus('weather-api', false, { error: error.message });
    }
  }

  async checkUsersApi(): Promise<HealthIndicatorResult> {
    try {
      await this.usersApi.getUsers(1, 1).toPromise();
      return this.getStatus('users-api', true);
    } catch (error) {
      return this.getStatus('users-api', false, { error: error.message });
    }
  }

  async checkAllApis(): Promise<HealthIndicatorResult[]> {
    const checks = await Promise.allSettled([
      this.checkWeatherApi(),
      this.checkUsersApi(),
    ]);

    return checks.map(check => 
      check.status === 'fulfilled' 
        ? check.value 
        : this.getStatus('api-check', false, { error: 'Check failed' })
    );
  }
}

// Health Check Controller
@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private externalApiHealth: ExternalApiHealthIndicator,
  ) {}

  @Get('external-apis')
  @HealthCheck()
  checkExternalApis() {
    return this.health.check([
      () => this.externalApiHealth.checkWeatherApi(),
      () => this.externalApiHealth.checkUsersApi(),
    ]);
  }
}

13.7.4 Monitoring und Metriken

@Injectable()
export class ApiMetricsService {
  private requestCount = 0;
  private successCount = 0;
  private errorCount = 0;
  private totalResponseTime = 0;

  recordRequest(
    method: string,
    url: string,
    statusCode: number,
    responseTime: number,
  ): void {
    this.requestCount++;
    this.totalResponseTime += responseTime;

    if (statusCode >= 200 && statusCode < 300) {
      this.successCount++;
    } else {
      this.errorCount++;
    }

    console.log(`API Metric: ${method} ${url} - ${statusCode} (${responseTime}ms)`);
  }

  getMetrics(): {
    total: number;
    success: number;
    errors: number;
    averageResponseTime: number;
    successRate: number;
  } {
    return {
      total: this.requestCount,
      success: this.successCount,
      errors: this.errorCount,
      averageResponseTime: this.requestCount > 0 
        ? this.totalResponseTime / this.requestCount 
        : 0,
      successRate: this.requestCount > 0 
        ? (this.successCount / this.requestCount) * 100 
        : 0,
    };
  }

  reset(): void {
    this.requestCount = 0;
    this.successCount = 0;
    this.errorCount = 0;
    this.totalResponseTime = 0;
  }
}

Die Integration externer APIs erfordert sorgfältige Planung und Implementierung robuster Patterns für Fehlerbehandlung, Caching und Monitoring. Durch die Verwendung der in diesem Kapitel vorgestellten Techniken können Sie zuverlässige und performante API-Integrationen in Ihren NestJS-Anwendungen erstellen.