HttpClient rămâne baza comunicării cu serverul. Ce s-a schimbat: din v21 e disponibil fără configurare, din v22 folosește Fetch API implicit, interceptorii sunt funcții, iar pentru citiri reactive ai httpResource. Toate demo-urile HTTP din acest site merg pe un backend simulat de un interceptor (core/api/mock-backend.ts).
- v21: serviciile HTTP sunt furnizate în root —
inject(HttpClient)merge fărăprovideHttpClient(). - v22:
FetchBackendimplicit;withFetch()deprecat; pentru progres la upload:withXhr(). - v22:
reportProgress→reportUploadProgress/reportDownloadProgress. - v22:
httpResourcestabil; TransferCache (SSR) nu mai include implicit request-urile cu credențiale.
Configurare
// v21: HttpClient e disponibil în root fără nicio configurare
export class UsersApi {
private readonly http = inject(HttpClient); // funcționează direct
}
// provideHttpClient() îl apelezi doar ca să CONFIGUREZI
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor]),
// withXhr(), // v22: Fetch e implicit; XHR doar dacă ai nevoie de progres la upload
),
],
};Interceptori funcționali
// Interceptor funcțional: o funcție (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); // metadate per request
const loading = inject(LoadingStore);
loading.start();
return next(req).pipe(finalize(() => loading.stop()));
};
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
// inject() doar aici, sincron: în catchError nu mai suntem în contextul de injecție
const router = inject(Router);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) router.navigate(['/login']);
return throwError(() => err);
}),
);
};Componenta tahttp.get('/api/users')
- Apasă „Trimite”: fiecare linie e scrisă de codul interceptorului, chiar în momentul în care rulează.
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
retryInterceptor,
cacheInterceptor,
fakeServer, // doar în demo: serverul simulat
]),
)Interceptorii retry și cache din laborator, fără liniile de jurnal:
// Reîncearcă doar erorile de server (5xx), de două ori, cu o pauză
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
if (req.method !== 'GET') return next(req); // un POST nu se repetă niciodată automat
return next(req).pipe(
retry({
count: 2,
delay: (error: HttpErrorResponse) =>
error.status >= 500 ? timer(300) : throwError(() => error),
}),
);
};
// într-un serviciu, nu într-o variabilă de modul: pe server ar fi comună tuturor utilizatorilor
@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);
}
}
// Păstrează răspunsurile GET; la hit, restul lanțului nu mai rulează deloc
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);
}),
);
};Stratul de API
Componentele nu ar trebui să știe URL-uri sau forma DTO-urilor. Un serviciu de API per resursă le ascunde și întoarce modele de domeniu (vezi Interfețe & modele).
// data-access/products-api.ts — URL-uri, DTO-uri și mapare într-un singur loc
@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));
}
}- Fă un request.
Citire vs scriere
// CITIRE reactivă → httpResource (în componentă sau store)
readonly product = httpResource(() => `/api/products/${this.id()}`, { parse: toProduct });
// SCRIERE → HttpClient (o comandă, nu o stare)
async save() {
const saved = await firstValueFrom(this.api.create(this.form().value()));
this.router.navigate(['/products', saved.id]);
}Schimbări în v22
// v22: FetchBackend implicit (withFetch() e deprecat — îl poți șterge)
provideHttpClient(); // folosește fetch()
// Progres la upload? Fetch nu îl suportă → XHR explicit
provideHttpClient(withXhr());
this.http.post('/api/upload', file, { reportUploadProgress: true, observe: 'events' });
// reportProgress e deprecat → reportUploadProgress / reportDownloadProgress
// v22.1: JSONP e deprecat (HttpClient.jsonp, HttpClientJsonpModule)Capcane frecvente
- Request-uri „reci”: un
http.get()fărăsubscribe(saufirstValueFrom) nu pleacă niciodată. - Subscribe în subscribe pentru request-uri dependente → folosește
switchMapsau resurse înlănțuite (ctx.chain). - Upload cu progres după upgrade la v22: Fetch nu raportează progresul de upload →
withXhr(). - Interceptori în ordinea greșită: se aplică în ordinea din array (auth înainte de logging etc.).
- Erori tratate de două ori: interceptorul afișează un toast, componenta încă unul. Decide un singur loc pentru fiecare tip de eroare.