HttpClient bleibt die Basis der Kommunikation mit dem Server. Was sich geändert hat: Ab v21 ist er ohne Konfiguration verfügbar, ab v22 nutzt er standardmäßig die Fetch API, Interceptors sind Funktionen, und für reaktives Lesen gibt es httpResource. Alle HTTP-Beispiele auf dieser Seite laufen auf einem von einem Interceptor simulierten Backend (core/api/mock-backend.ts).
- v21: HTTP-Services werden in Root bereitgestellt —
inject(HttpClient)funktioniert ohneprovideHttpClient(). - v22:
FetchBackendist der Standard;withFetch()ist deprecated; für den Upload-Fortschritt:withXhr(). - v22:
reportProgress→reportUploadProgress/reportDownloadProgress. - v22:
httpResourceist stabil; der TransferCache (SSR) enthält standardmäßig keine Requests mit Credentials mehr.
Konfiguration
// v21: HttpClient ist in Root ganz ohne Konfiguration verfügbar
export class UsersApi {
private readonly http = inject(HttpClient); // funktioniert direkt
}
// provideHttpClient() rufst du nur auf, um ihn zu KONFIGURIEREN
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor]),
// withXhr(), // v22: Fetch ist der Standard; XHR nur, wenn du den Upload-Fortschritt brauchst
),
],
};Funktionale Interceptors
// Funktionaler Interceptor: eine Funktion (req, next) => Observable
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthStore).token();
if (!token || !req.url.startsWith('/api')) return next(req);
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
if (req.context.get(SKIP_LOADING)) return next(req); // Metadaten pro Request
const loading = inject(LoadingStore);
loading.start();
return next(req).pipe(finalize(() => loading.stop()));
};
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
// inject() nur hier, synchron: innerhalb von catchError sind wir nicht mehr in einem Injection-Kontext
const router = inject(Router);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) router.navigate(['/login']);
return throwError(() => err);
}),
);
};Deine Komponentehttp.get('/api/users')
- Klicke auf „Senden“: Jede Zeile schreibt der Code des Interceptors selbst, genau in dem Moment, in dem er läuft.
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
retryInterceptor,
cacheInterceptor,
fakeServer, // nur in der Demo: der simulierte Server
]),
)Die retry- und cache-Interceptors aus dem Labor, ohne die Log-Zeilen:
// Wiederholt nur Serverfehler (5xx), zweimal, mit Pause
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
if (req.method !== 'GET') return next(req); // einen POST nie automatisch wiederholen
return next(req).pipe(
retry({
count: 2,
delay: (error: HttpErrorResponse) =>
error.status >= 500 ? timer(300) : throwError(() => error),
}),
);
};
// in einem Service, nicht in einer Modulvariable: auf dem Server teilten sie sich alle Nutzer
@Service()
export class ResponseCache {
private readonly entries = new Map<string, HttpResponse<unknown>>();
get(key: string) {
return this.entries.get(key);
}
set(key: string, response: HttpResponse<unknown>) {
this.entries.set(key, response);
}
}
// Merkt sich GET-Antworten; bei einem Treffer läuft der Rest der Kette gar nicht
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
if (req.method !== 'GET') return next(req);
const cache = inject(ResponseCache);
const hit = cache.get(req.urlWithParams);
if (hit) return of(hit.clone());
return next(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) cache.set(req.urlWithParams, event);
}),
);
};Die API-Schicht
Komponenten sollten weder URLs noch die Form der DTOs kennen. Ein Service pro Ressource versteckt sie und liefert Domänenmodelle zurück (siehe Interfaces & Modelle).
// data-access/products-api.ts — URLs, DTOs und Mapping an einem Ort
@Service()
export class ProductsApi {
private readonly http = inject(HttpClient);
list(q = ''): Observable<Product[]> {
return this.http
.get<PageDto<ProductDto>>('/api/products', { params: { q } })
.pipe(map((page) => page.items.map(toProduct)));
}
get(id: number): Observable<Product> {
return this.http.get<ProductDto>(`/api/products/${id}`).pipe(
map(toProduct),
retry({ count: 2, delay: (_, attempt) => timer(300 * attempt) }), // backoff
);
}
create(body: CreateProductRequest): Observable<Product> {
return this.http.post<ProductDto>('/api/products', body).pipe(map(toProduct));
}
}- Sende einen Request.
Lesen vs. Schreiben
// Reaktives LESEN → httpResource (in einer Komponente oder einem Store)
readonly product = httpResource(() => `/api/products/${this.id()}`, { parse: toProduct });
// SCHREIBEN → HttpClient (ein Befehl, kein Zustand)
async save() {
const saved = await firstValueFrom(this.api.create(this.form().value()));
this.router.navigate(['/products', saved.id]);
}Änderungen in v22
// v22: FetchBackend ist der Standard (withFetch() ist deprecated — du kannst es entfernen)
provideHttpClient(); // nutzt fetch()
// Upload-Fortschritt? Fetch unterstützt ihn nicht → explizit XHR
provideHttpClient(withXhr());
this.http.post('/api/upload', file, { reportUploadProgress: true, observe: 'events' });
// reportProgress ist deprecated → reportUploadProgress / reportDownloadProgress
// v22.1: JSONP ist deprecated (HttpClient.jsonp, HttpClientJsonpModule)Häufige Stolperfallen
- „Kalte“ Requests: Ein
http.get()ohnesubscribe(oderfirstValueFrom) wird nie gesendet. - Subscribe in Subscribe bei abhängigen Requests → verwende
switchMapoder verkettete Resources (ctx.chain). - Upload mit Fortschritt nach dem Upgrade auf v22: Fetch meldet den Upload-Fortschritt nicht →
withXhr(). - Interceptors in der falschen Reihenfolge: Sie werden in der Reihenfolge des Arrays angewendet (Auth vor Logging usw.).
- Fehler doppelt behandelt: Der Interceptor zeigt einen Toast, und die Komponente zeigt einen weiteren. Lege für jede Art von Fehler eine einzige Stelle fest.