18 Server Kommunikation in Angular

Angular bietet umfangreiche Mechanismen zur Backend-Server-Kommunikation, insbesondere für die Interaktion mit RESTful APIs, GraphQL-Endpunkten und WebSockets. Dieser Leitfaden behandelt die grundlegenden und erweiterten Methoden zur Datenverarbeitung mit CRUD-Operationen (Create, Read, Update, Delete) und berücksichtigt die aktuellen Features von Angular.

18.1 Einführung

Die Serverkommunikation in Angular basiert hauptsächlich auf dem HttpClient, der Teil des @angular/common/http-Pakets ist. Die bevorzugte Methode besteht darin, spezialisierte Services zu erstellen, die diese Funktionalität kapseln und als Datenquelle für Komponenten und andere Services dienen.

18.2 Das HttpClientModule und Standalone Components

In Angular haben sich die Importmechanismen im Vergleich zu früheren Versionen verändert. Mit dem Standalone-Konzept von Angular müssen die Module nicht mehr im AppModule importiert werden.

18.2.1 Traditioneller Ansatz (immer noch unterstützt)

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';

@NgModule({
    declarations: [AppComponent],
    imports: [BrowserModule, HttpClientModule],
    bootstrap: [AppComponent]
})
export class AppModule {}

18.2.2 Moderner Standalone-Ansatz (empfohlen)

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';

bootstrapApplication(AppComponent, {
    providers: [
        provideHttpClient(withInterceptorsFromDi())
    ]
}).catch(err => console.error(err));

Bei der Verwendung von Standalone-Komponenten wird HttpClient direkt über den Provider injiziert, nicht über ein Modulimport:

// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
    providedIn: 'root'
})
export class DataService {
    constructor(private http: HttpClient) {}
    // Service-Methoden hier
}

18.3 REST-Operationen mit HttpClient

HttpClient unterstützt alle gängigen HTTP-Methoden, die für RESTful APIs erforderlich sind:

18.3.1 GET-Anfragen

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../models/user.model';

@Injectable({
    providedIn: 'root'
})
export class UserService {
    private apiUrl = 'https://api.example.com/users';

    constructor(private http: HttpClient) {}

    // Alle Benutzer abrufen
    getAllUsers(): Observable<User[]> {
        return this.http.get<User[]>(this.apiUrl);
    }

    // Einen bestimmten Benutzer nach ID abrufen
    getUserById(id: number): Observable<User> {
        return this.http.get<User>(`${this.apiUrl}/${id}`);
    }

    // Benutzer mit Filterparametern abrufen
    getFilteredUsers(nameFilter: string, role: string): Observable<User[]> {
        let params = new HttpParams()
            .set('name', nameFilter)
            .set('role', role);

        return this.http.get<User[]>(this.apiUrl, { params });
    }
}

18.3.2 Retry- und Fallback-Strategien

Angular bietet umfangreiche Möglichkeiten zur Behandlung von Netzwerkfehlern:

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, of, timer } from 'rxjs';
import { retry, retryWhen, delayWhen, tap, catchError, finalize, switchMap, scan } from 'rxjs/operators';

@Injectable({
    providedIn: 'root'
})
export class ResilienceApiService {
    constructor(
        private http: HttpClient,
        private offlineService: OfflineStorageService
    ) {}

    /**
     * GET-Request mit automatischen Wiederholungen und exponentieller Backoff-Strategie
     */
    getWithRetry<T>(url: string): Observable<T> {
        return this.http.get<T>(url).pipe(
            // Einfache Wiederholung mit fester Anzahl
            // retry(3),

            // Oder: Erweiterte Wiederholungsstrategie mit exponentieller Verzögerung
            retryWhen(errors =>
                errors.pipe(
                    // Anzahl der Versuche zählen
                    scan((attempts, error) => {
                        // Maximale Anzahl Versuche erreicht oder nicht wiederholbarer Fehler
                        if (attempts >= 5 || error.status === 400 || error.status === 404) {
                            throw error;
                        }
                        return attempts + 1;
                    }, 0),
                    // Exponentieller Backoff: 1s, 2s, 4s, 8s, ...
                    delayWhen(attempts => timer(Math.pow(2, attempts) * 1000)),
                    tap(attempts => console.log(`Retry attempt ${attempts}`))
                )
            ),
            // Fallback, wenn alle Versuche fehlschlagen
            catchError(this.handleFailure.bind(this))
        );
    }

    /**
     * Fehlerbehandlung mit Offline-Fallback
     */
    private handleFailure<T>(error: HttpErrorResponse): Observable<T> {
        // Netzwerkfehler oder Offline-Status
        if (error.error instanceof ErrorEvent || !navigator.onLine) {
            console.log('Network error or offline. Trying offline data...');

            // Versuch, Daten aus dem lokalen Speicher zu laden
            const offlineData = this.offlineService.getOfflineData<T>(error.url);

            if (offlineData) {
                // Offline-Daten als Fallback
                return of(offlineData).pipe(
                    tap(() => console.log('Using offline data for', error.url))
                );
            }
        }

        // Keine Offline-Daten verfügbar oder anderer Fehler
        return throwError(() => error);
    }
}

18.3.3 POST-Anfragen

// Neuen Benutzer erstellen
createUser(user: User): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
}

// Datei hochladen
uploadUserAvatar(userId: number, file: File): Observable<any> {
    const formData = new FormData();
    formData.append('avatar', file, file.name);

    return this.http.post(`${this.apiUrl}/${userId}/avatar`, formData, {
        reportProgress: true,
        observe: 'events'
    });
}

18.3.4 PUT und PATCH-Anfragen

// Vollständige Aktualisierung eines Benutzers (PUT)
updateUser(id: number, user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${id}`, user);
}

// Teilweise Aktualisierung eines Benutzers (PATCH)
partialUpdateUser(id: number, partialUser: Partial<User>): Observable<User> {
    return this.http.patch<User>(`${this.apiUrl}/${id}`, partialUser);
}

18.3.5 DELETE-Anfragen

// Benutzer löschen
deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
}

// Löschen mit Anfragekörper (weniger üblich, aber manchmal erforderlich)
deleteUsersInBulk(userIds: number[]): Observable<void> {
    return this.http.delete<void>(this.apiUrl, {
        body: { ids: userIds }
    });
}

18.4 Angular Signals API für reaktive Server-Kommunikation

Angular bietet das Signals API als wichtige Funktion für Reaktivität. Es lässt sich nahtlos in die Server-Kommunikation integrieren:

import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { catchError, switchMap, finalize, tap } from 'rxjs/operators';
import { EMPTY, Observable } from 'rxjs';

interface Product {
    id: number;
    name: string;
    price: number;
    category: string;
}

@Injectable({
    providedIn: 'root'
})
export class ProductSignalService {
    // Signal für den ausgewählten Filter
    private categoryFilter = signal<string | null>(null);

    // Computed Signal für die API-URL basierend auf dem Filter
    private apiUrl = computed(() => {
        const baseUrl = 'https://api.example.com/products';
        const filter = this.categoryFilter();
        return filter ? `${baseUrl}?category=${filter}` : baseUrl;
    });

    // Konvertieren des Signals in ein Observable für HTTP-Anfragen
    private apiUrl$ = toObservable(this.apiUrl);

    // Products-Observable basierend auf der URL
    private products$ = this.apiUrl$.pipe(
        switchMap(url =>
            this.http.get<Product[]>(url).pipe(
                catchError(error => {
                    console.error('Error fetching products', error);
                    return EMPTY;
                })
            )
        )
    );

    // Konvertieren des Observables zurück in ein Signal für die Komponenten
    readonly products = toSignal<Product[], Product[]>(this.products$, { initialValue: [] });

    // Anzahl der Produkte als berechnetes Signal
    readonly productCount = computed(() => this.products().length);

    // Signal für den Ladezustand
    readonly loading = signal<boolean>(false);

    // Signal für Fehlerzustände
    readonly error = signal<string | null>(null);

    constructor(private http: HttpClient) {}

    /**
     * Kategorie-Filter aktualisieren
     */
    setCategory(category: string | null): void {
        this.categoryFilter.set(category);
    }

    /**
     * Produkt abrufen mit Signal-basiertem Ansatz
     */
    getProduct(id: number): Observable<Product> {
        this.loading.set(true);
        this.error.set(null);

        return this.http.get<Product>(`https://api.example.com/products/${id}`).pipe(
            catchError(error => {
                this.error.set(`Failed to load product #${id}: ${error.message}`);
                throw error;
            }),
            finalize(() => {
                this.loading.set(false);
            })
        );
    }

    /**
     * Produkt hinzufügen und Produktliste manuell aktualisieren
     */
    addProduct(product: Omit<Product, 'id'>): Observable<Product> {
        return this.http.post<Product>('https://api.example.com/products', product).pipe(
            tap(newProduct => {
                // Aktuelles Array erhalten
                const currentProducts = this.products();
                // Array aktualisieren durch Hinzufügen des neuen Produkts
                // Signals verwenden Änderungserkennung durch Referenzgleichheit
                const updatedProducts = [...currentProducts, newProduct];
                // Hinweis: In echten Anwendungen würde man stattdessen das Observable
                // neu abonnieren, um konsistente Daten zu gewährleisten
            })
        );
    }
}

18.4.1 Verwendung in einer Komponente

@Component({
    selector: 'app-product-list',
    standalone: true,
    imports: [CommonModule],
    template: `
    <div class="filters">
      <button (click)="setCategory(null)">All</button>
      <button (click)="setCategory('electronics')">Electronics</button>
      <button (click)="setCategory('books')">Books</button>
    </div>
    
    <div *ngIf="productService.loading()">Loading...</div>
    <div *ngIf="productService.error()">{{ productService.error() }}</div>
    
    <div class="product-count">
      Showing {{ productService.productCount() }} products
    </div>
    
    <ul class="products">
      @for (product of productService.products(); track product.id) {
        <li>
          {{ product.name }} - {{ product.price | currency }}
        </li>
      }
    </ul>
  `
})
export class ProductListComponent {
    constructor(public productService: ProductSignalService) {}

    setCategory(category: string | null): void {
        this.productService.setCategory(category);
    }
}

18.5 Behandlung von Observables und fortgeschrittene RxJS-Techniken

18.5.1 Grundlegende Subscription

@Component({
    selector: 'app-user-list',
    standalone: true,
    imports: [CommonModule],
    template: `
    <div *ngIf="loading">Loading users...</div>
    <ul *ngIf="!loading">
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
    <div *ngIf="error">{{ error }}</div>
  `
})
export class UserListComponent implements OnInit, OnDestroy {
    users: User[] = [];
    loading = false;
    error = '';
    private subscription: Subscription | null = null;

    constructor(private userService: UserService) {}

    ngOnInit() {
        this.loading = true;
        this.subscription = this.userService.getAllUsers().subscribe({
            next: (data) => {
                this.users = data;
                this.loading = false;
            },
            error: (err) => {
                this.error = 'Failed to load users: ' + err.message;
                this.loading = false;
            }
        });
    }

    ngOnDestroy() {
        this.subscription?.unsubscribe();
    }
}

18.5.2 Verwendung von async Pipe (empfohlen)

Der moderne Angular-Ansatz verwendet die async-Pipe, um Subscriptions automatisch zu verwalten:

@Component({
    selector: 'app-user-list',
    standalone: true,
    imports: [CommonModule],
    template: `
    <ng-container *ngIf="users$ | async as users; else loading">
      <ul>
        <li *ngFor="let user of users">{{ user.name }}</li>
      </ul>
    </ng-container>
    <ng-template #loading>Loading users...</ng-template>
  `
})
export class UserListComponent {
    users$ = this.userService.getAllUsers().pipe(
        catchError(err => {
            this.errorMessage = err.message;
            return EMPTY;
        })
    );
    errorMessage = '';

    constructor(private userService: UserService) {}
}

18.5.3 RxJS-Operatoren für erweiterte Szenarien

Angular fördert die Verwendung von RxJS-Operatoren für komplexe Datenmanipulationen:

import { Component } from '@angular/core';
import { Observable, combineLatest, EMPTY, BehaviorSubject } from 'rxjs';
import { map, switchMap, catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
    selector: 'app-user-search',
    standalone: true,
    imports: [CommonModule, ReactiveFormsModule],
    template: `
    <input [formControl]="searchInput" placeholder="Search users...">
    <select [formControl]="roleFilter">
      <option value="">All Roles</option>
      <option value="admin">Admin</option>
      <option value="user">User</option>
    </select>
    
    <div *ngIf="loading$ | async">Searching...</div>
    
    <ul *ngIf="filteredUsers$ | async as users">
      <li *ngFor="let user of users">{{ user.name }} ({{ user.role }})</li>
    </ul>
  `
})
export class UserSearchComponent {
    searchInput = new FormControl('');
    roleFilter = new FormControl('');

    private searchTerms$ = this.searchInput.valueChanges.pipe(
        debounceTime(300),
        distinctUntilChanged()
    );

    private roleSelection$ = this.roleFilter.valueChanges;

    private loadingSubject = new BehaviorSubject<boolean>(false);
    loading$ = this.loadingSubject.asObservable();

    filteredUsers$: Observable<User[]> = combineLatest([
        this.searchTerms$,
        this.roleSelection$
    ]).pipe(
        switchMap(([term, role]) => {
            this.loadingSubject.next(true);
            return this.userService.getFilteredUsers(term || '', role || '').pipe(
                catchError(error => {
                    console.error('Error fetching users', error);
                    this.loadingSubject.next(false);
                    return EMPTY;
                }),
                map(users => {
                    this.loadingSubject.next(false);
                    return users;
                })
            );
        })
    );

    constructor(private userService: UserService) {}
}

18.6 Error Handling und Retry-Mechanismen

Angular bietet verbesserte Möglichkeiten zur zentralen Fehlerbehandlung:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, finalize } from 'rxjs/operators';
import { LoadingService } from './loading.service';
import { NotificationService } from './notification.service';

@Injectable({
    providedIn: 'root'
})
export class ApiService {
    constructor(
        private http: HttpClient,
        private loadingService: LoadingService,
        private notificationService: NotificationService
    ) {}

    get<T>(url: string, options = {}): Observable<T> {
        this.loadingService.show();

        return this.http.get<T>(url, options).pipe(
            retry(2), // Automatisch bis zu zweimal wiederholen bei Netzwerkfehlern
            catchError(error => {
                this.handleError(error);
                return throwError(() => error);
            }),
            finalize(() => this.loadingService.hide())
        );
    }

    private handleError(error: any): void {
        let errorMessage = '';

        if (error.error instanceof ErrorEvent) {
            // Client-seitiger Fehler
            errorMessage = `Error: ${error.error.message}`;
        } else {
            // Backend-Fehler
            errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;

            // Spezifische HTTP-Statuscodes behandeln
            if (error.status === 401) {
                this.notificationService.warn('You need to log in again');
                // this.authService.logout(); // Benutzer ausloggen, wenn die Sitzung abgelaufen ist
            } else if (error.status === 403) {
                this.notificationService.error('You do not have permission to access this resource');
            } else if (error.status === 404) {
                this.notificationService.warn('The requested resource was not found');
            } else if (error.status >= 500) {
                this.notificationService.error('A server error occurred, please try again later');
            }
        }

        console.error(errorMessage);
        this.notificationService.error(errorMessage);
    }
}

18.7 CORS (Cross-Origin Resource Sharing) in Angular

CORS ist ein kritischer Aspekt bei der Entwicklung von Angular-Anwendungen, die mit Backend-APIs kommunizieren. Da Angular-Apps typischerweise auf einem anderen Port (4200) als das Backend laufen, treten CORS-Probleme häufig während der Entwicklung auf.

18.7.1 CORS-Grundlagen für Angular-Entwickler

CORS-Fehler sind eine der häufigsten Herausforderungen in der Angular-Entwicklung. Sie treten auf, wenn der Browser Cross-Origin-Requests blockiert:

// Typischer CORS-Fehler in der Konsole:
// Access to XMLHttpRequest at 'http://localhost:3000/api/users' 
// from origin 'http://localhost:4200' has been blocked by CORS policy

18.7.2 CORS-Behandlung in Angular-Services

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Observable, throwError, of } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class CorsHandlingService {
  constructor(private http: HttpClient) {}

  /**
   * Service-Methode mit CORS-Error-Handling
   */
  getDataWithCorsHandling<T>(endpoint: string): Observable<T> {
    const url = `${environment.apiBaseUrl}/${endpoint}`;
    
    return this.http.get<T>(url).pipe(
      catchError((error: HttpErrorResponse) => {
        // CORS-Fehler erkennen und behandeln
        if (this.isCorsError(error)) {
          console.warn('CORS error detected, trying alternative approach');
          return this.handleCorsError<T>(endpoint);
        }
        
        return throwError(() => error);
      })
    );
  }

  /**
   * CORS-Fehler erkennen
   */
  private isCorsError(error: HttpErrorResponse): boolean {
    return error.status === 0 && 
           error.error instanceof ProgressEvent &&
           error.error.type === 'error';
  }

  /**
   * CORS-Fallback-Strategien
   */
  private handleCorsError<T>(endpoint: string): Observable<T> {
    // Option 1: Proxy-Server verwenden (Development)
    if (!environment.production) {
      const proxyUrl = `/api/${endpoint}`;
      return this.http.get<T>(proxyUrl);
    }

    // Option 2: JSONP für GET-Requests (falls Backend unterstützt)
    if (endpoint.includes('jsonp-supported')) {
      return this.http.jsonp<T>(`${environment.apiBaseUrl}/${endpoint}`, 'callback');
    }

    // Option 3: Alternative API-Endpunkte
    const fallbackUrls = environment.fallbackApiUrls || [];
    if (fallbackUrls.length > 0) {
      return this.tryFallbackUrls<T>(endpoint, fallbackUrls);
    }

    return throwError(() => new Error('CORS error and no fallback available'));
  }

  private tryFallbackUrls<T>(endpoint: string, urls: string[]): Observable<T> {
    return urls.reduce((prev, url) => {
      return prev.pipe(
        catchError(() => this.http.get<T>(`${url}/${endpoint}`))
      );
    }, throwError(() => new Error('No fallback worked')));
  }
}

18.7.3 Angular Proxy-Konfiguration für Entwicklung

Der beste Ansatz für CORS-Probleme in der Entwicklung ist die Verwendung des Angular CLI Proxy:

// proxy.conf.json
{
  "/api/*": {
    "target": "http://localhost:3000",
    "secure": true,
    "changeOrigin": true,
    "logLevel": "debug"
  },
  "/auth/*": {
    "target": "http://localhost:3001",
    "secure": true,
    "changeOrigin": true
  }
}
// angular.json - serve-Konfiguration erweitern
"serve": {
  "builder": "@angular-devkit/build-angular:dev-server",
  "options": {
    "proxyConfig": "proxy.conf.json"
  }
}
# Angular Dev Server mit Proxy starten
ng serve --proxy-config proxy.conf.json

18.7.4 Erweiterte Proxy-Konfiguration

// proxy.conf.js - Für komplexere Proxy-Regeln
const PROXY_CONFIG = [
  {
    context: ['/api', '/auth'],
    target: 'http://localhost:3000',
    secure: false,
    changeOrigin: true,
    logLevel: 'debug',
    onProxyReq: (proxyReq, req, res) => {
      // Custom Headers für Backend hinzufügen
      proxyReq.setHeader('X-Forwarded-Host', req.headers.host);
      proxyReq.setHeader('X-Forwarded-Proto', 'http');
    },
    onProxyRes: (proxyRes, req, res) => {
      // Response-Headers modifizieren
      proxyRes.headers['Access-Control-Allow-Origin'] = '*';
    }
  },
  {
    context: ['/uploads'],
    target: 'http://localhost:3002',
    secure: false,
    changeOrigin: true
  }
];

module.exports = PROXY_CONFIG;

18.7.5 Environment-spezifische CORS-Konfiguration

// environments/environment.ts
export const environment = {
  production: false,
  apiBaseUrl: '/api', // Verwendet Proxy in Development
  corsEnabled: false,
  fallbackApiUrls: ['http://backup-api.example.com']
};

// environments/environment.prod.ts
export const environment = {
  production: true,
  apiBaseUrl: 'https://api.example.com',
  corsEnabled: true,
  fallbackApiUrls: []
};

18.7.6 CORS-bewusster HTTP-Service

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private defaultHeaders = new HttpHeaders({
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  });

  constructor(private http: HttpClient) {}

  /**
   * GET-Request mit CORS-Überlegungen
   */
  get<T>(endpoint: string, params?: HttpParams): Observable<T> {
    const options = {
      headers: this.defaultHeaders,
      params,
      // withCredentials nur setzen, wenn Backend es explizit erlaubt
      withCredentials: environment.corsEnabled && environment.production
    };

    return this.http.get<T>(`${environment.apiBaseUrl}/${endpoint}`, options);
  }

  /**
   * POST-Request mit Preflight-Bewusstsein
   */
  post<T>(endpoint: string, data: any): Observable<T> {
    // In Development: einfache Headers verwenden, um Preflight zu vermeiden
    const headers = environment.production 
      ? this.defaultHeaders.set('Authorization', 'Bearer ' + this.getToken())
      : new HttpHeaders({ 'Content-Type': 'application/json' });

    return this.http.post<T>(`${environment.apiBaseUrl}/${endpoint}`, data, { headers });
  }

  private getToken(): string {
    return localStorage.getItem('authToken') || '';
  }
}

18.7.7 CORS-Debugging und -Monitoring

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap, catchError, finalize } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class CorsMonitoringService {
  constructor(private http: HttpClient) {}

  makeRequestWithMonitoring<T>(url: string): Observable<T> {
    const startTime = performance.now();
    
    console.log(`🚀 Making request to: ${url}`);
    console.log(`📍 Current origin: ${window.location.origin}`);
    console.log(`🎯 Target origin: ${new URL(url).origin}`);
    console.log(`❓ Cross-origin: ${window.location.origin !== new URL(url).origin}`);

    return this.http.get<T>(url).pipe(
      tap(response => {
        const duration = performance.now() - startTime;
        console.log(`✅ Request successful in ${duration.toFixed(2)}ms`);
        console.log('📥 Response:', response);
      }),
      catchError(error => {
        const duration = performance.now() - startTime;
        console.error(`❌ Request failed after ${duration.toFixed(2)}ms`);
        
        if (error.status === 0) {
          console.error('🚫 Likely CORS error - check browser network tab');
          console.error('💡 Solutions:');
          console.error('   1. Configure CORS on the server');
          console.error('   2. Use Angular proxy in development');
          console.error('   3. Deploy frontend and backend on same domain');
        }
        
        throw error;
      }),
      finalize(() => {
        console.log(`🏁 Request to ${url} completed`);
      })
    );
  }
}

18.7.8 Best Practices für CORS in Angular

// service/cors-best-practices.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class CorsOptimizedService {
  constructor(private http: HttpClient) {}

  /**
   * Optimierte API-Calls für CORS-Umgebungen
   */
  optimizedApiCall<T>(endpoint: string, data?: any): Observable<T> {
    // 1. Minimale Headers verwenden, um Preflight zu vermeiden
    const headers = new HttpHeaders({
      'Content-Type': 'application/json'
      // Keine custom Headers in einfachen Requests
    });

    // 2. Credentials nur wenn nötig
    const options = {
      headers,
      withCredentials: false // Standardmäßig false
    };

    // 3. Timeout für bessere UX
    const request = data 
      ? this.http.post<T>(endpoint, data, options)
      : this.http.get<T>(endpoint, options);

    return request.pipe(
      timeout(5000), // 5 Sekunden Timeout
      catchError(this.handleCorsError.bind(this))
    );
  }

  private handleCorsError(error: any): Observable<never> {
    if (error.name === 'TimeoutError') {
      console.error('Request timeout - possibly due to CORS preflight delay');
    }
    
    return throwError(() => error);
  }
}

18.8 Interceptors für Headers, Authentication und Logging

Interceptors sind leistungsstarke Werkzeuge zur Manipulation von HTTP-Anfragen und -Antworten:

import { Injectable } from '@angular/core';
import {
    HttpInterceptor,
    HttpRequest,
    HttpHandler,
    HttpEvent,
    HttpResponse,
    HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private authService: AuthService) {}

    intercept(
        request: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<any>> {

        // Authentifizierungstoken hinzufügen
        const token = this.authService.getToken();

        if (token) {
            request = request.clone({
                setHeaders: {
                    Authorization: `Bearer ${token}`
                }
            });
        }

        // Einheitliche Content-Type für Nicht-FormData-Anfragen setzen
        if (!(request.body instanceof FormData)) {
            request = request.clone({
                setHeaders: {
                    'Content-Type': 'application/json'
                }
            });
        }

        // Anfragen- und Antwortprotokolle für Debugging
        return next.handle(request).pipe(
            tap({
                next: (event) => {
                    if (event instanceof HttpResponse) {
                        console.log('API Response', {
                            url: request.url,
                            status: event.status,
                            body: event.body
                        });
                    }
                }
            }),
            catchError((error: HttpErrorResponse) => {
                // Spezifische Authentifizierungsfehler behandeln
                if (error.status === 401) {
                    this.authService.refreshToken().subscribe({
                        next: () => {
                            // Token erneuert, Anfrage wiederholen
                            // Implementierungsdetails hier
                        },
                        error: () => {
                            // Token-Erneuerung fehlgeschlagen, Benutzer ausloggen
                            this.authService.logout();
                        }
                    });
                }

                return throwError(() => error);
            })
        );
    }
}

18.8.1 Registrieren von Interceptors

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './app/auth.interceptor';
import { LoggingInterceptor } from './app/logging.interceptor';
import { CacheInterceptor } from './app/cache.interceptor';

bootstrapApplication(AppComponent, {
    providers: [
        { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true },
        provideHttpClient(withInterceptorsFromDi())
    ]
}).catch(err => console.error(err));

18.9 Fortgeschrittene HTTP-Features in Angular

18.9.1 Progress Events für Up- und Downloads

import { Component } from '@angular/core';
import { HttpClient, HttpEventType } from '@angular/common/http';
import { finalize } from 'rxjs/operators';

@Component({
    selector: 'app-file-upload',
    standalone: true,
    template: `
    <input type="file" (change)="onFileSelected($event)">
    <button (click)="uploadFile()" [disabled]="!selectedFile">Upload</button>
    
    <div *ngIf="progress > 0">
      Upload progress: {{ progress }}%
      <div class="progress-bar" [style.width.%]="progress"></div>
    </div>
  `,
    styles: [`
    .progress-bar {
      height: 5px;
      background-color: blue;
      transition: width 0.3s;
    }
  `]
})
export class FileUploadComponent {
    selectedFile: File | null = null;
    progress = 0;

    constructor(private http: HttpClient) {}

    onFileSelected(event: Event): void {
        const input = event.target as HTMLInputElement;
        if (input.files?.length) {
            this.selectedFile = input.files[0];
        }
    }

    uploadFile(): void {
        if (!this.selectedFile) return;

        const formData = new FormData();
        formData.append('file', this.selectedFile);

        this.http.post('https://api.example.com/upload', formData, {
            reportProgress: true,
            observe: 'events'
        }).pipe(
            finalize(() => {
                this.selectedFile = null;
                setTimeout(() => this.progress = 0, 3000); // Reset progress after 3s
            })
        ).subscribe(event => {
            if (event.type === HttpEventType.UploadProgress) {
                this.progress = Math.round(100 * event.loaded / (event.total || 1));
            } else if (event.type === HttpEventType.Response) {
                console.log('Upload successful', event.body);
            }
        });
    }
}

18.9.2 Response Types und Options

// JSON ist der Standard, aber andere Formate werden unterstützt
downloadPdf(documentId: string): Observable<Blob> {
    return this.http.get(`${this.apiUrl}/documents/${documentId}`, {
        responseType: 'blob'
    });
}

// Text-Response
fetchRawText(url: string): Observable<string> {
    return this.http.get(url, {
        responseType: 'text'
    });
}

// ArrayBuffer für Binärdaten
fetchBinaryData(url: string): Observable<ArrayBuffer> {
    return this.http.get(url, {
        responseType: 'arraybuffer'
    });
}

// Vollständige Response mit Header-Informationen
getWithFullResponse(url: string): Observable<HttpResponse<any>> {
    return this.http.get(url, {
        observe: 'response'
    });
}

18.9.3 HttpContext für kontextabhängige Request-Konfiguration

Angular führt erweiterte Kontextfunktionen ein:

import { HttpContext, HttpContextToken } from '@angular/common/http';

// Tokens definieren, die als Flags oder Werte dienen
const BYPASS_CACHE = new HttpContextToken(() => false);
const PRIORITY = new HttpContextToken(() => 'normal');

// In einem Interceptor verwenden
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // Prüfen, ob das Caching für diese Anfrage übersprungen werden soll
        if (req.context.get(BYPASS_CACHE)) {
            console.log(`Bypassing cache for: ${req.url}`);
            return next.handle(req);
        }

        // Priorisierung der Anfrage basierend auf dem Kontextwert
        const priority = req.context.get(PRIORITY);
        console.log(`Request priority: ${priority} for: ${req.url}`);

        // Implementierung der Caching-Logik hier...
        return next.handle(req);
    }
}

// Im Service verwenden
getLatestData(forceRefresh = false): Observable<Data> {
    return this.http.get<Data>('https://api.example.com/data', {
        context: new HttpContext()
            .set(BYPASS_CACHE, forceRefresh)
            .set(PRIORITY, forceRefresh ? 'high' : 'normal')
    });
}

18.10 WebSockets in Angular

Angular bietet keine eingebaute WebSocket-Implementierung, aber Sie können die nativen Browser-WebSockets oder Bibliotheken wie socket.io-client verwenden:

import { Injectable } from '@angular/core';
import { Observable, Subject, Observer } from 'rxjs';
import { environment } from 'src/environments/environment';

export interface Message {
    type: string;
    data: any;
}

@Injectable({
    providedIn: 'root'
})
export class WebSocketService {
    private socket!: WebSocket;
    private messageSubject = new Subject<Message>();
    public messages$ = this.messageSubject.asObservable();

    constructor() {}

    connect(): Observable<boolean> {
        return new Observable((observer: Observer<boolean>) => {
            this.socket = new WebSocket(environment.wsUrl);

            this.socket.onopen = () => {
                console.log('WebSocket connected');
                observer.next(true);
                observer.complete();
            };

            this.socket.onerror = (error) => {
                console.error('WebSocket error:', error);
                observer.error(error);
            };

            this.socket.onmessage = (event) => {
                try {
                    const message = JSON.parse(event.data);
                    this.messageSubject.next(message);
                } catch (err) {
                    console.error('Error parsing WebSocket message', err);
                }
            };

            this.socket.onclose = () => {
                console.log('WebSocket connection closed');
            };
        });
    }

    sendMessage(message: Message): void {
        if (this.socket?.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify(message));
        } else {
            console.error('WebSocket is not connected. Cannot send message.');
        }
    }

    close(): void {
        this.socket?.close();
    }
}

18.10.1 SignalR-Integration für Echtzeit-Kommunikation

Angular arbeitet gut mit SignalR für Echtzeit-Updates zusammen:

// npm install @microsoft/signalr

import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class SignalRService {
  private hubConnection!: HubConnection;
  private connectionStatus = new BehaviorSubject<boolean>(false);
  private messageSubject = new Subject<any>();
  
  public connectionStatus$ = this.connectionStatus.asObservable();
  public messages$ = this.messageSubject.asObservable();
  
  constructor() {
    this.createConnection();
  }
  
  private createConnection(): void {
    this.hubConnection = new HubConnectionBuilder()
      .withUrl(`${environment.apiBaseUrl}/hub`)
      .withAutomaticReconnect([0, 1000, 5000, 10000, 30000]) // Wiederverbindungsversuche
      .configureLogging(LogLevel.Information)
      .build();
    
    // Status-Ereignisse
    this.hubConnection.onreconnecting(() => {
      console.log('Reconnecting to SignalR hub...');
      this.connectionStatus.next(false);
    });
    
    this.hubConnection.onreconnected(() => {
      console.log('Reconnected to SignalR hub');
      this.connectionStatus.next(true);
    });
    
    this.hubConnection.onclose(() => {
      console.log('Connection closed');
      this.connectionStatus.next(false);
    });
  }
  
  public connect(): Promise<void> {
    if (this.hubConnection.state === 'Disconnected') {
      return this.hubConnection.start()
        .then(() => {
          console.log('SignalR Connected');
          this.connectionStatus.next(true);
          this.registerEventHandlers();
        })
        .catch(err => {
          console.error('Error starting SignalR connection', err);
          this.connectionStatus.next(false);
          throw err;
        });
    }
    
    return Promise.resolve();
  }
  
  public disconnect(): Promise<void> {
    if (this.hubConnection.state === 'Connected') {
      return this.hubConnection.stop();
    }
    return Promise.resolve();
  }
  
  // Event-Handler registrieren
  private registerEventHandlers(): void {
    // Nachrichten vom Server
    this.hubConnection.on('ReceiveMessage', (message: any) => {
      this.messageSubject.next(message);
    });
    
    // Benachrichtigungen
    this.hubConnection.on('ReceiveNotification', (notification: any) => {
      console.log('New notification', notification);
      // Weiterleitung an Notification-Service etc.
    });
    
    // Aktualisierungen für Echtzeit-Daten
    this.hubConnection.on('DataUpdated', (data: any) => {
      // Cache invalidieren oder aktualisieren
    });
  }
  
  // Methode auf dem Server aufrufen
  public invokeServerMethod(methodName: string, ...args: any[]): Promise<any> {
    return this.ensureConnection()
      .then(() => this.hubConnection.invoke(methodName, ...args))
      .catch(error => {
        console.error(`Error calling ${methodName}`, error);
        throw error;
      });
  }
  
  // Sicherstellen, dass eine Verbindung besteht
  private ensureConnection(): Promise<void> {
    if (this.hubConnection.state === 'Connected') {
      return Promise.resolve();
    } else {
      return this.connect();
    }
  }
}

// Verwendung in einer Chat-Komponente
@Component({
  selector: 'app-chat',
  template: `
    <div class="connection-status" [class.connected]="isConnected">
      {{ isConnected ? 'Connected' : 'Disconnected' }}
    </div>
    
    <div class="chat-messages">
      <div *ngFor="let msg of messages" class="message" [class.own-message]="msg.sender === currentUser">
        <div class="sender">{{ msg.sender }}</div>
        <div class="content">{{ msg.content }}</div>
        <div class="time">{{ msg.timestamp | date:'shortTime' }}</div>
      </div>
    </div>
    
    <div class="chat-input">
      <input [(ngModel)]="messageText" placeholder="Type a message..." 
             (keyup.enter)="sendMessage()">
      <button (click)="sendMessage()" [disabled]="!isConnected || !messageText.trim()">Send</button>
    </div>
  `
})
export class ChatComponent implements OnInit, OnDestroy {
  isConnected = false;
  messages: any[] = [];
  messageText = '';
  currentUser = 'CurrentUser'; // In einer echten App aus dem AuthService holen
  
  private subscriptions: Subscription[] = [];
  
  constructor(private signalRService: SignalRService) {}
  
  ngOnInit(): void {
    // Verbindung herstellen
    this.signalRService.connect();
    
    // Verbindungsstatus überwachen
    this.subscriptions.push(
      this.signalRService.connectionStatus$.subscribe(status => {
        this.isConnected = status;
      })
    );
    
    // Nachrichten empfangen
    this.subscriptions.push(
      this.signalRService.messages$.subscribe(message => {
        this.messages.push(message);
        // Automatisch nach unten scrollen
        setTimeout(() => this.scrollToBottom(), 0);
      })
    );
  }
  
  sendMessage(): void {
    if (!this.messageText.trim() || !this.isConnected) return;
    
    // Nachricht an den Server senden
    this.signalRService.invokeServerMethod('SendMessage', {
      sender: this.currentUser,
      content: this.messageText,
      timestamp: new Date()
    }).then(() => {
      this.messageText = '';
    }).catch(error => {
      console.error('Error sending message', error);
    });
  }
  
  private scrollToBottom(): void {
    // Implementierung zum Scrollen zum Ende des Chat-Fensters
  }
  
  ngOnDestroy(): void {
    // Alle Subscriptions aufräumen
    this.subscriptions.forEach(sub => sub.unsubscribe());
    // SignalR-Verbindung trennen
    this.signalRService.disconnect();
  }
}

## GraphQL in Angular

Für GraphQL-Integrationen wird häufig die Apollo-Client-Bibliothek verwendet:

```typescript
// Installation erforderlich:
// npm install @apollo/client graphql apollo-angular

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideApolloClient } from './apollo/apollo-client.provider';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideApolloClient()
  ]
};

// apollo/apollo-client.provider.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core';
import { environment } from '../environments/environment';

export function provideApolloClient() {
  return () => {
    return new ApolloClient({
      link: new HttpLink({
        uri: environment.graphqlUrl
      }),
      cache: new InMemoryCache()
    });
  };
}

// user.graphql.service.ts
import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '../models/user.model';

// GraphQL Queries und Mutations definieren
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      role
    }
  }
`;

const GET_USER_BY_ID = gql`
  query GetUserById($id: ID!) {
    user(id: $id) {
      id
      name
      email
      role
      createdAt
      lastLogin
      settings {
        theme
        notifications
      }
    }
  }
`;

const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
      role
    }
  }
`;

@Injectable({
  providedIn: 'root'
})
export class UserGraphQLService {
  constructor(private apollo: Apollo) {}

  getUsers(): Observable<User[]> {
    return this.apollo.watchQuery<any>({
      query: GET_USERS
    }).valueChanges.pipe(
      map(result => result.data.users)
    );
  }

  getUserById(id: string): Observable<User> {
    return this.apollo.watchQuery<any>({
      query: GET_USER_BY_ID,
      variables: { id }
    }).valueChanges.pipe(
      map(result => result.data.user)
    );
  }

  createUser(user: Omit<User, 'id'>): Observable<User> {
    return this.apollo.mutate<any>({
      mutation: CREATE_USER,
      variables: {
        input: user
      },
      update: (cache, { data }) => {
        // Cache aktualisieren, um neue Daten zu reflektieren
        const existingUsers = cache.readQuery<any>({ query: GET_USERS });
        
        if (existingUsers && existingUsers.users) {
          cache.writeQuery({
            query: GET_USERS,
            data: {
              users: [...existingUsers.users, data.createUser]
            }
          });
        }
      }
    }).pipe(
      map(result => result.data.createUser)
    );
  }
}

18.11 Best Practices für Angular Server-Kommunikation

18.11.1 Modellierung von Datentypen mit TypeScript-Interfaces

// models/user.model.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: string;
  lastLogin?: string;
  isActive: boolean;
  settings?: UserSettings;
}

export interface UserSettings {
  theme: 'light' | 'dark' | 'system';
  notifications: boolean;
  language: string;
}

// Mit Generics für API-Antworten
export interface ApiResponse<T> {
  data: T;
  meta: {
    timestamp: string;
    status: number;
    message: string;
  };
}

export interface PaginatedResponse<T> {
  items: T[];
  page: number;
  totalPages: number;
  totalItems: number;
}

18.11.2 Service-Modularisierung und Strukturierung

// Basis-Service für alle API-Endpunkte
@Injectable({
  providedIn: 'root'
})
export class BaseApiService {
  constructor(protected http: HttpClient) {}

  protected get<T>(endpoint: string, options = {}): Observable<T> {
    return this.http.get<T>(this.buildUrl(endpoint), options).pipe(
      catchError(this.handleError)
    );
  }

  protected post<T>(endpoint: string, data: any, options = {}): Observable<T> {
    return this.http.post<T>(this.buildUrl(endpoint), data, options).pipe(
      catchError(this.handleError)
    );
  }

  // Weitere Basismethoden...

  private buildUrl(endpoint: string): string {
    return `${environment.apiBaseUrl}/${endpoint}`;
  }

  private handleError(error: HttpErrorResponse): Observable<never> {
    // Zentrale Fehlerbehandlung
    console.error('API Error', error);
    return throwError(() => error);
  }
}

// Spezialisierter Service für Benutzer-Endpunkte
@Injectable({
  providedIn: 'root'
})
export class UserService extends BaseApiService {
  constructor(http: HttpClient) {
    super(http);
  }

  getUsers(): Observable<User[]> {
    return this.get<User[]>('users');
  }

  getUserById(id: number): Observable<User> {
    return this.get<User>(`users/${id}`);
  }

  // Weitere spezifische Methoden...
}

18.11.3 Caching-Strategien mit HTTP-Request-Caching

Effizientes Caching reduziert Netzwerkanfragen und verbessert die Leistung:

// services/cache.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class CacheService {
  private cache = new Map<string, {
    data: any;
    timestamp: number;
    expiresIn: number;
  }>();

  constructor() {
    // Optional: Periodisches Aufräumen des Caches einrichten
    setInterval(() => this.clearExpired(), 60 * 1000); // Jede Minute
  }

  /**
   * Speichert Daten im Cache
   * @param key Cache-Schlüssel
   * @param data Zu speichernde Daten
   * @param expiresIn Ablaufzeit in Millisekunden (Standard: 5 Minuten)
   */
  set(key: string, data: any, expiresIn = 5 * 60 * 1000): void {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      expiresIn
    });
  }

  /**
   * Ruft Daten aus dem Cache ab
   * @param key Cache-Schlüssel
   * @returns Die gespeicherten Daten oder null, wenn kein gültiger Eintrag vorhanden ist
   */
  get(key: string): any | null {
    const entry = this.cache.get(key);
    
    if (!entry) {
      return null;
    }

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

    return entry.data;
  }

  /**
   * Löscht einen Cache-Eintrag
   * @param key Cache-Schlüssel
   */
  delete(key: string): void {
    this.cache.delete(key);
  }

  /**
   * Löscht alle abgelaufenen Cache-Einträge
   */
  clearExpired(): void {
    const now = Date.now();
    this.cache.forEach((entry, key) => {
      if (now > entry.timestamp + entry.expiresIn) {
        this.cache.delete(key);
      }
    });
  }

  /**
   * Löscht den gesamten Cache
   */
  clearAll(): void {
    this.cache.clear();
  }

  /**
   * Löscht alle Cache-Einträge, die einen bestimmten Präfix im Schlüssel haben
   * @param keyPrefix Der Präfix des Schlüssels
   */
  clearByPrefix(keyPrefix: string): void {
    this.cache.forEach((_, key) => {
      if (key.startsWith(keyPrefix)) {
        this.cache.delete(key);
      }
    });
  }
}

// services/cached-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { CacheService } from './cache.service';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class CachedApiService {
  constructor(
    private http: HttpClient,
    private cacheService: CacheService
  ) {}

  /**
   * Führt eine GET-Anfrage mit Caching durch
   * @param endpoint API-Endpunkt
   * @param options HTTP-Optionen
   * @param cacheTime Cache-Gültigkeitsdauer in ms (Standard: 5 Minuten)
   * @param forceFresh Ignoriert den Cache und holt frische Daten
   */
  cachedGet<T>(
    endpoint: string, 
    options = {}, 
    cacheTime = 5 * 60 * 1000,
    forceFresh = false
  ): Observable<T> {
    const url = `${environment.apiBaseUrl}/${endpoint}`;
    const cacheKey = `get:${url}:${JSON.stringify(options)}`;
    
    // Cache prüfen, wenn nicht forceFresh
    if (!forceFresh) {
      const cachedData = this.cacheService.get(cacheKey);
      if (cachedData !== null) {
        return of(cachedData);
      }
    }
    
    // Daten vom Server holen und cachen
    return this.http.get<T>(url, options).pipe(
      tap(response => {
        this.cacheService.set(cacheKey, response, cacheTime);
      }),
      catchError(error => {
        console.error('Error fetching data:', error);
        throw error;
      })
    );
  }

  /**
   * Cache für einen bestimmten Endpunkt invalidieren
   * @param endpoint Der zu invalidieren Endpunkt
   */
  invalidateCache(endpoint: string): void {
    const prefix = `get:${environment.apiBaseUrl}/${endpoint}`;
    this.cacheService.clearByPrefix(prefix);
  }
}

// Beispiel für die Verwendung:
@Injectable({
  providedIn: 'root'
})
export class ProductService {
  constructor(
    private cachedApi: CachedApiService,
    private http: HttpClient
  ) {}

  // Produktliste mit Caching (5 Minuten)
  getProducts(): Observable<Product[]> {
    return this.cachedApi.cachedGet<Product[]>('products');
  }
  
  // Produktdetails mit längerem Caching (1 Stunde)
  getProductById(id: number): Observable<Product> {
    return this.cachedApi.cachedGet<Product>(
      `products/${id}`, 
      {}, 
      60 * 60 * 1000
    );
  }
  
  // Produkt aktualisieren und Cache invalidieren
  updateProduct(product: Product): Observable<Product> {
    const endpoint = `products/${product.id}`;
    return this.http.put<Product>(
      `${environment.apiBaseUrl}/${endpoint}`, 
      product
    ).pipe(
      tap(() => {
        // Sowohl Liste als auch Einzelprodukt-Cache löschen
        this.cachedApi.invalidateCache('products');
        this.cachedApi.invalidateCache(endpoint);
      })
    );
  }
}