20 REST APIs in Angular

20.1 Grundlagen der REST-Kommunikation

REST (Representational State Transfer) ist ein Architekturstil für verteilte Systeme, der 2000 von Roy Fielding in seiner Dissertation eingeführt wurde. REST definiert einen Satz von Prinzipien, die beschreiben, wie Netzwerkressourcen definiert und adressiert werden können.

Die Hauptmerkmale von REST-APIs sind:

20.2 HttpClient in Angular

Der HttpClient ist Angulars primäres Werkzeug für die Kommunikation mit REST-APIs. Seit Angular 4.3 ist er Teil des @angular/common/http-Pakets und bietet eine moderne, leistungsstarke HTTP-Client-Implementierung.

20.2.1 Einrichtung des HttpClient

Um den HttpClient zu verwenden, müssen Sie das HttpClientModule in Ihrem App-Modul importieren:

// 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  // Import hier hinzufügen
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

In Angular 14+ mit der Standalone-API können Sie den HttpClient auch direkt in der Komponente oder im Service importieren:

// app.component.ts (Angular 14+)
import { Component } from '@angular/core';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { CommonModule } from '@angular/common';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [CommonModule, HttpClientModule],
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    // ...
}

Bei Angular 16+ wird die Verwendung von provideHttpClient() in der bootstrapApplication-Methode empfohlen:

// main.ts (Angular 16+)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient } from '@angular/common/http';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient()
  ]
});

20.3 HTTP-Methoden und ihre Anwendung

20.3.1 GET-Anfragen

GET-Anfragen werden verwendet, um Ressourcen vom Server abzurufen. Der HttpClient bietet typisierte Antworten, die die Arbeit mit zurückgegebenen Daten erheblich erleichtern:

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } 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) { }

  // Typisierte Antwort mit einer Benutzerliste
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }

  // Abrufen eines einzelnen Benutzers mit einer ID
  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
}

20.3.2 POST-Anfragen

POST-Anfragen werden verwendet, um neue Ressourcen zu erstellen:

// user.service.ts (Ergänzung)
createUser(user: User): Observable<User> {
  return this.http.post<User>(this.apiUrl, user);
}

20.3.3 PUT und PATCH-Anfragen

PUT aktualisiert eine gesamte Ressource, während PATCH nur Teile einer Ressource aktualisiert:

// user.service.ts (Ergänzung)
// Vollständiges Update eines Benutzers
updateUser(user: User): Observable<User> {
  return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}

// Partielles Update eines Benutzers
partialUpdateUser(id: number, changes: Partial<User>): Observable<User> {
  return this.http.patch<User>(`${this.apiUrl}/${id}`, changes);
}

20.3.4 DELETE-Anfragen

DELETE-Anfragen werden verwendet, um Ressourcen zu entfernen:

// user.service.ts (Ergänzung)
deleteUser(id: number): Observable<void> {
  return this.http.delete<void>(`${this.apiUrl}/${id}`);
}

20.4 Erweiterte Funktionen

20.4.1 Request-Konfiguration und Parameter

Der HttpClient erlaubt die Anpassung von Anfragen durch Konfigurationsoptionen:

// user.service.ts (Ergänzung)
import { HttpParams, HttpHeaders } from '@angular/common/http';

// Anfrage mit URL-Parametern
getUsersWithPagination(page: number, pageSize: number): Observable<User[]> {
  const params = new HttpParams()
    .set('page', page.toString())
    .set('pageSize', pageSize.toString());
    
  return this.http.get<User[]>(this.apiUrl, { params });
}

// Anfrage mit benutzerdefinierten Headers
getUserWithCustomHeaders(id: number): Observable<User> {
  const headers = new HttpHeaders()
    .set('Content-Type', 'application/json')
    .set('Authorization', 'Bearer your-token-here');
    
  return this.http.get<User>(`${this.apiUrl}/${id}`, { headers });
}

20.4.2 Fehlerbehandlung

Die RxJS-Operatoren catchError und throwError sind nützlich für die Behandlung von HTTP-Fehlern:

// error-handling.service.ts
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlingService {
  handleError(error: HttpErrorResponse): Observable<never> {
    let errorMessage = 'Ein unbekannter Fehler ist aufgetreten!';
    
    if (error.error instanceof ErrorEvent) {
      // Client-seitiger Fehler
      errorMessage = `Fehler: ${error.error.message}`;
    } else {
      // Server-seitiger Fehler
      errorMessage = `Statuscode: ${error.status}, Nachricht: ${error.message}`;
    }
    
    console.error(errorMessage);
    return throwError(() => new Error(errorMessage));
  }
}

// user.service.ts (mit Fehlerbehandlung)
import { catchError } from 'rxjs/operators';

// Ergänzung zum UserService
constructor(
  private http: HttpClient,
  private errorService: ErrorHandlingService
) { }

getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl)
    .pipe(
      catchError(error => this.errorService.handleError(error))
    );
}

20.4.3 Abbruch von Anfragen

Seit Angular 10 ist es möglich, HTTP-Anfragen während der Ausführung über ein AbortController-Signal abzubrechen, was in Angular 11 und höher standardmäßig unterstützt wird:

// user.service.ts (Ergänzung)
getUserWithTimeout(id: number): Observable<User> {
  // Erzeugen eines AbortSignal, das nach 5 Sekunden die Anfrage abbricht
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);

  return this.http.get<User>(`${this.apiUrl}/${id}`, {
    signal: controller.signal
  }).pipe(
    finalize(() => clearTimeout(timeoutId))
  );
}

20.5 HTTP-Interceptoren

Interceptoren sind ein mächtiges Feature von Angular, um HTTP-Anfragen und -Antworten global zu manipulieren. Sie werden häufig für Authentifizierung, Logging oder Fehlerbehandlung verwendet.

20.5.1 Authentifizierungs-Interceptor

// auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

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

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    // Fügt den Auth-Token zu jeder ausgehenden Anfrage hinzu
    const token = this.auth.getToken();
    
    if (token) {
      const authRequest = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
      return next.handle(authRequest);
    }
    
    return next.handle(request);
  }
}

20.5.2 Registrierung des Interceptors

In Angular < 15:

// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';

@NgModule({
  // ...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class AppModule { }

In Angular 15+ mit der Provider-Funktion:

// main.ts (Angular 15+)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './app/interceptors/auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor])
    )
  ]
});

In Angular 16+ kann der Interceptor auch als Funktion definiert werden:

// auth.interceptor.ts (Angular 16+)
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();
  
  if (token) {
    const authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
    return next(authReq);
  }
  
  return next(req);
};

20.6 Fortgeschrittene Konzepte

20.6.1 Reaktive Programmierung mit RxJS

Angular HTTP kombiniert hervorragend mit RxJS-Operatoren für komplexe Datenverarbeitung:

// user.service.ts (mit RxJS-Operatoren)
import { map, tap, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { Observable, Subject, combineLatest } from 'rxjs';

// Suchfunktion mit Debounce
private searchTerms = new Subject<string>();

search(term: string): void {
  this.searchTerms.next(term);
}

getSearchResults(): Observable<User[]> {
  return this.searchTerms.pipe(
    debounceTime(300),  // Warte 300ms nach letzter Eingabe
    distinctUntilChanged(),  // Ignoriere doppelte Suchbegriffe
    switchMap(term => this.http.get<User[]>(`${this.apiUrl}/search?q=${term}`))
  );
}

// Daten kombinieren und transformieren
getUserWithPosts(userId: number): Observable<{user: User, posts: Post[]}> {
  const user$ = this.http.get<User>(`${this.apiUrl}/users/${userId}`);
  const posts$ = this.http.get<Post[]>(`${this.apiUrl}/posts?userId=${userId}`);
  
  return combineLatest([user$, posts$]).pipe(
    map(([user, posts]) => ({ user, posts }))
  );
}

20.6.2 Cache-Implementierung

Eine einfache Cache-Implementierung mit RxJS shareReplay:

// cache.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { shareReplay, tap, catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class CacheService {
  private cache: Record<string, Observable<any>> = {};
  
  constructor(private http: HttpClient) { }
  
  get<T>(url: string, options?: any): Observable<T> {
    if (!this.cache[url]) {
      this.cache[url] = this.http.get<T>(url, options).pipe(
        shareReplay(1),  // Cache das letzte Ergebnis
        catchError(err => {
          delete this.cache[url];  // Fehlerhafte Antworten nicht cachen
          return throwError(() => err);
        })
      );
    }
    
    return this.cache[url];
  }
  
  clearCache(): void {
    this.cache = {};
  }
  
  clearCacheEntry(url: string): void {
    delete this.cache[url];
  }
}

20.6.3 GraphQL mit Apollo Client in Angular

Obwohl REST das dominierende API-Paradigma ist, gewinnt GraphQL an Popularität. Angular arbeitet gut mit dem Apollo-Client zusammen:

// Beispiel zur Integration von Apollo Client mit Angular
// app.module.ts
import { APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache } from '@apollo/client/core';

@NgModule({
  // ...
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: (httpLink: HttpLink) => {
        return {
          cache: new InMemoryCache(),
          link: httpLink.create({
            uri: 'https://api.example.com/graphql',
          }),
        };
      },
      deps: [HttpLink],
    },
  ],
})
export class AppModule {}

// user.service.ts (mit Apollo)
import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

@Injectable({
  providedIn: 'root'
})
export class UserGraphQLService {
  constructor(private apollo: Apollo) { }
  
  getUsers(): Observable<User[]> {
    return this.apollo.query<{users: User[]}>({
      query: GET_USERS
    }).pipe(
      map(result => result.data.users)
    );
  }
}

20.7 Neuerungen in Angular 17+

20.7.1 Signal-basierte HTTP-Anfragen (Angular 17+)

Mit der Einführung von Signals in Angular 16+ und deren Weiterentwicklung in Angular 17+ können HTTP-Anfragen nun eleganter mit Signals integriert werden:

// user.service.ts (Angular 17+)
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';

@Injectable({
  providedIn: 'root'
})
export class UserSignalService {
  private apiUrl = 'https://api.example.com/users';
  
  // Signals für den Zustand
  private usersSignal = signal<User[]>([]);
  private loadingSignal = signal<boolean>(false);
  private errorSignal = signal<string | null>(null);
  
  // Öffentliche readonly-Signale
  public users = this.usersSignal.asReadonly();
  public loading = this.loadingSignal.asReadonly();
  public error = this.errorSignal.asReadonly();
  
  // Berechnetes Signal für gefilterte Benutzer
  public activeUsers = computed(() => 
    this.users().filter(user => user.isActive)
  );
  
  constructor(private http: HttpClient) {}
  
  loadUsers(): void {
    this.loadingSignal.set(true);
    this.errorSignal.set(null);
    
    this.http.get<User[]>(this.apiUrl).subscribe({
      next: (users) => {
        this.usersSignal.set(users);
        this.loadingSignal.set(false);
      },
      error: (err) => {
        this.errorSignal.set(err.message || 'Ein Fehler ist aufgetreten');
        this.loadingSignal.set(false);
      }
    });
  }
  
  // Alternative mit toSignal
  users$ = this.http.get<User[]>(this.apiUrl);
  usersSignalAlt = toSignal(this.users$, { initialValue: [] as User[] });
}

20.7.2 Verbesserte HTTP-Konfiguration (Angular 18)

Angular 18 bietet verbesserte HTTP-Konfigurationsoptionen, insbesondere für Standalone-Anwendungen:

// main.ts (Angular 18)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient, withFetch, withJsonpSupport, withXsrfConfiguration } from '@angular/common/http';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      // Verwendet Fetch API statt XMLHttpRequest
      withFetch(),
      // JSONP-Unterstützung für Cross-Origin-Anfragen
      withJsonpSupport(),
      // XSRF-Konfiguration
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',
        headerName: 'X-XSRF-TOKEN'
      })
    )
  ]
});

20.8 Best Practices für REST in Angular

20.8.1 Strukturierung von API-Services

Eine gut strukturierte Anwendung teilt die API-Kommunikation in spezialisierte Services auf:

// api.service.ts - Basis-Service für alle API-Aufrufe
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private baseUrl = environment.apiUrl;
  
  constructor(private http: HttpClient) { }
  
  get<T>(path: string, options = {}): Observable<T> {
    return this.http.get<T>(`${this.baseUrl}${path}`, options);
  }
  
  post<T>(path: string, body: any, options = {}): Observable<T> {
    return this.http.post<T>(`${this.baseUrl}${path}`, body, options);
  }
  
  put<T>(path: string, body: any, options = {}): Observable<T> {
    return this.http.put<T>(`${this.baseUrl}${path}`, body, options);
  }
  
  delete<T>(path: string, options = {}): Observable<T> {
    return this.http.delete<T>(`${this.baseUrl}${path}`, options);
  }
}

// user.service.ts - Spezialisierter Service für Benutzer-APIs
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from './api.service';
import { User } from '../models/user.model';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private path = '/users';
  
  constructor(private api: ApiService) { }
  
  getAll(): Observable<User[]> {
    return this.api.get<User[]>(this.path);
  }
  
  getById(id: number): Observable<User> {
    return this.api.get<User>(`${this.path}/${id}`);
  }
  
  create(user: User): Observable<User> {
    return this.api.post<User>(this.path, user);
  }
  
  update(user: User): Observable<User> {
    return this.api.put<User>(`${this.path}/${user.id}`, user);
  }
  
  delete(id: number): Observable<void> {
    return this.api.delete<void>(`${this.path}/${id}`);
  }
}

20.8.2 Datenzugriff in Komponenten

Moderne Angular-Komponenten sollten observables von Services abonnieren und Async-Pipe verwenden:

// users.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { UserService } from '../services/user.service';
import { User } from '../models/user.model';

@Component({
  selector: 'app-users',
  template: `
    <div *ngIf="users$ | async as users; else loading">
      <div *ngFor="let user of users">
        {{ user.name }} - {{ user.email }}
      </div>
    </div>
    <ng-template #loading>Laden...</ng-template>
  `
})
export class UsersComponent implements OnInit {
  users$: Observable<User[]>;
  
  constructor(private userService: UserService) { }
  
  ngOnInit(): void {
    this.users$ = this.userService.getAll();
  }
}

Oder mit Signals (Angular 17+):

// users.component.ts (Angular 17+)
import { Component, OnInit, inject } from '@angular/core';
import { UserSignalService } from '../services/user-signal.service';

@Component({
  selector: 'app-users',
  standalone: true,
  imports: [CommonModule],
  template: `
    @if (userService.loading()) {
      <div>Laden...</div>
    } @else if (userService.error()) {
      <div class="error">{{ userService.error() }}</div>
    } @else {
      <div *ngFor="let user of userService.users()">
        {{ user.name }} - {{ user.email }}
      </div>
    }
  `
})
export class UsersComponent implements OnInit {
  userService = inject(UserSignalService);
  
  ngOnInit(): void {
    this.userService.loadUsers();
  }
}

Die REST-API-Kommunikation in Angular hat sich über die letzten Versionen hinweg deutlich verbessert. Während die grundlegenden HTTP-Methoden (GET, POST, PUT, DELETE) konstant geblieben sind, wurde die API um fortgeschrittene Funktionen wie typisierte Antworten, Interceptoren und verbesserte Fehlerbehandlung erweitert.

Mit Angular 16+ wurden Signals eingeführt, die eine elegantere und reaktivere Möglichkeit bieten, mit REST-API-Daten zu arbeiten. Außerdem haben sich die Konfigurationsoptionen für HTTP-Clients erweitert, insbesondere für Standalone-Anwendungen.

Unabhängig von der Angular-Version sollten Entwickler darauf achten, ihre API-Kommunikation zu strukturieren, typsicher zu gestalten und angemessene Fehlerbehandlung zu implementieren, um robuste und wartbare Anwendungen zu erstellen.