Angular HttpClient ile API İstekleri: GET, POST, Hata Yönetimi ve Interceptor
Angular 12'de HttpClient ile API'ye GET, POST, PUT ve DELETE istekleri atıyoruz. Tipli response, HttpParams, catchError ve retry ile hata yönetimi, class-based interceptor ve environment ayarı.
Sürüm notu: Bu yazıdaki kod ve komutlar Angular 12 içindir.
Veri Başka Bir Yerde Yaşıyor
Şimdiye kadar yazdığımız component'lerde veri hep class'ın içindeydi: elle yazılmış bir ürün listesi, sabit bir kullanıcı adı. Gerçek uygulamalarda işler böyle yürümez. Ürünler bir veritabanında, kullanıcılar bir sunucuda durur ve Angular uygulaması onlara HTTP üzerinden ulaşır.
Bunu bir kurye gibi düşün. Sen evdesin, depo şehrin öbür ucunda. Kurye paketi alır, getirir; adres yanlışsa ya da depo kapalıysa eli boş döner ve sana haber verir. Angular'da bu kuryenin adı HttpClient. Bu yazıda onu kuracağız, ona dört temel işi yaptıracağız, hataları yakalayacağız ve her isteğin geçtiği ortak kapıyı, yani interceptor'ı yazacağız. Backend yazmadan deneme yapabilmek için örneklerde JSONPlaceholder adlı ücretsiz sahte API'yi kullanacağız.
HttpClientModule'ü Uygulamaya Tanıtmak
HttpClient, @angular/common/http paketiyle gelir ama kendiliğinden hazır değildir. Önce module'ünü AppModule'e eklemen gerekir:
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 { }
Bu import olmadan HttpClient'ı bir service'e inject etmeye çalışırsan tarayıcı konsolunda No provider for HttpClient! ile biten bir NullInjectorError görürsün. Mesaj aslında derdini açıkça söylüyor: injector, HttpClient'ı üretecek bir provider bulamadı. HttpClientModule'ü import ettiğinde bu provider'lar uygulamaya eklenir. Bunu bir kez, kök module'de yapman yeterli.
İlk GET İsteği ve Tipli Response
HTTP çağrılarını component'in içine yazmayız, bu iş service'in. Daha önce gördüğümüz providedIn: 'root' service'lerden birini src/app/user.service.ts olarak açalım ve önce gelecek verinin şeklini tanımlayalım:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
constructor(private http: HttpClient) { }
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
}
User interface'i, sunucudan gelecek her kullanıcının hangi alanlara sahip olduğunu TypeScript'e anlatıyor. JSONPlaceholder aslında çok daha fazla alan döndürür, biz sadece kullanacaklarımızı yazdık. HttpClient ise constructor üzerinden, tıpkı başka herhangi bir service gibi inject ediliyor.
Asıl iş get<User[]> satırında. Köşeli parantezdeki generic tip "bu isteğin cevabı bir kullanıcı dizisi olacak" demek. Bunu yazmazsan dönüş tipi Object olur ve component tarafında user.name yazdığın anda derleyici itiraz eder. Yalnız şunu unutma: bu tip bir söz, bir kontrol değil. Sunucu başka şekilde bir veri gönderirse TypeScript bunu çalışma anında yakalamaz.
Metot geriye bir Observable döndürüyor. Daha önce gördüğümüz gibi observable tembeldir; biri subscribe olana kadar ağa tek bir istek bile gitmez. Şimdi ng g c user-list ile bir component üretip service'i kullanalım:
import { Component, OnInit } from '@angular/core';
import { User, UserService } from '../user.service';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
users: User[] = [];
constructor(private userService: UserService) { }
ngOnInit(): void {
this.userService.getUsers().subscribe(users => this.users = users);
}
}
<ul>
<li *ngFor="let user of users">{{ user.name }} - {{ user.email }}</li>
</ul>
İsteği ngOnInit içinde başlatıyoruz, gelen diziyi users property'sine yazıyoruz, template de onu listeliyor. Tarayıcının Network sekmesini açarsan users adresine giden tek bir GET isteği görürsün.
Unsubscribe yazmadığımızı fark etmişsindir. HttpClient'ın döndürdüğü observable cevap geldiği anda kendiliğinden tamamlanır, sürekli akan bir stream değildir. Yine de component kapanırken istek hâlâ yoldaysa onu iptal etmek istersen, daha önce gördüğümüz async pipe ya da takeUntil deseni burada da iş görür.
POST, PUT ve DELETE
Okumak işin yarısı. Kayıt eklemek, güncellemek ve silmek için aynı service'e üç metot daha ekleyelim:
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
updateUser(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
post ikinci parametre olarak isteğin gövdesini alır. Düz bir JavaScript nesnesi verirsin, HttpClient onu JSON'a çevirir ve Content-Type: application/json header'ını kendisi ekler. Omit<User, 'id'> ise "id hariç bir User" demek; yeni kaydın id'sini sunucu verir. put hangi kaydı güncelleyeceğini URL'deki id'den anlar, delete ise gövde taşımaz, o yüzden dönüş tipini void yaptık.
Component tarafında kullanımı GET'ten farksız:
addUser(): void {
this.userService.createUser({ name: 'Ayşe Demir', email: 'ayse@example.com' })
.subscribe(created => this.users.push(created));
}
JSONPlaceholder kayıt edilmiş gibi davranır ve yeni kullanıcıyı id: 11 ile geri döndürür ama gerçekte hiçbir şey saklamaz. Deneme için bu kadarı yeterli.
Burada yeni başlayanların en sık takıldığı noktayı söyleyeyim: bu metotlar da subscribe olmadan çalışmaz. this.userService.deleteUser(3); yazıp bırakırsan hiçbir şey silinmez, çünkü istek hiç yola çıkmaz. Kuryeye adresi verip kapıdan çıkmasına izin vermemek gibi.
HttpParams ve HttpHeaders
Sorgu parametreleri ve header'lar için URL'i elle birleştirmek yerine Angular'ın iki yardımcı class'ını kullanırız:
import { HttpHeaders, HttpParams } from '@angular/common/http';
export interface Post {
id: number;
userId: number;
title: string;
}
getPostsByUser(userId: number): Observable<Post[]> {
const params = new HttpParams()
.set('userId', String(userId))
.set('_limit', '5');
const headers = new HttpHeaders({ 'Accept-Language': 'tr' });
return this.http.get<Post[]>('https://jsonplaceholder.typicode.com/posts', { params, headers });
}
Bu çağrı /posts?userId=1&_limit=5 adresine gider ve isteğe bir de Accept-Language header'ı eklenir. Değerler URL'e uygun şekilde kodlanır, soru işareti ve ampersand derdini sen düşünmezsin.
Dikkat edilmesi gereken tek şey şu: HttpParams ve HttpHeaders immutable'dır. set metodu mevcut nesneyi değiştirmez, yeni bir nesne döndürür. Zincirleme yazmamızın sebebi bu. params.set('page', '2'); yazıp sonucu bir değişkene atamazsan o parametre isteğe hiç eklenmez.
Hataları Yakalamak: catchError ve retry
İnternet kopar, sunucu çöker, adres yanlış yazılır. HTTP isteği her zaman başarılı dönmez ve bunu daha önce gördüğümüz RxJS operatörleriyle yönetiriz:
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl).pipe(
retry(2),
catchError((error: HttpErrorResponse) => {
console.error('Kullanıcılar alınamadı:', error.status, error.message);
return throwError(error);
})
);
}
retry(2) istek başarısız olursa iki kez daha dener; yani toplamda üç deneme. Anlık ağ dalgalanmalarında hayat kurtarır. Ama her isteğe körü körüne koyma: bir POST'u tekrar denemek aynı kaydı iki kez oluşturabilir.
catchError hatayı yakalar ve sana bir HttpErrorResponse verir. status alanı neyin ters gittiğini söyler: 404 adres yok, 500 sunucu tarafında sorun var, 0 ise istek sunucuya hiç ulaşamamış demektir. catchError mutlaka bir observable döndürmelidir. Burada hatayı logladıktan sonra throwError ile tekrar fırlatıyoruz ki component de haberdar olsun. İstersen of([]) ile boş bir liste döndürüp hatayı tamamen yutabilirsin. Angular 12 projeleri RxJS 6 ile geldiği için operatörleri rxjs/operators yolundan import ettiğimize de dikkat et.
Component'te hatayı karşılamak için subscribe'a bir nesne veriyoruz:
this.userService.getUsers().subscribe({
next: users => this.users = users,
error: () => this.errorMessage = 'Liste şu an yüklenemiyor, biraz sonra tekrar dene.'
});
errorMessage component'te '' başlangıç değeriyle tanımlı basit bir string. Template'te *ngIf ile gösterirsin, kullanıcı da boş bir ekrana bakıp ne olduğunu merak etmez.
Interceptor: Her İsteğin Geçtiği Kapı
Diyelim ki API her istekte bir token istiyor. Her service metoduna tek tek header eklemek hem yorucu hem de bir gün mutlaka unutulacak bir iş. Interceptor tam bunun için var: uygulamadan çıkan her HTTP isteği ve dönen her cevap yolda onun önünden geçer. Bir binanın girişindeki görevli gibi düşün; kim girerse girsin, yaka kartını o takar. CLI ile iskeletini üretelim:
ng generate interceptor auth
Oluşan dosyayı şöyle düzenle:
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = localStorage.getItem('token');
if (!token) {
return next.handle(request);
}
const authRequest = request.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next.handle(authRequest);
}
}
Class, HttpInterceptor interface'ini uyguluyor ve tek bir metodu var: intercept. Gelen request immutable olduğu için üzerine doğrudan header yazamayız; clone ile kopyasını alıp setHeaders ile header'ı ekliyoruz. next.handle ise isteği zincirdeki bir sonraki interceptor'a, sıradaki kalmadıysa da sunucuya iletir. Token yoksa isteğe dokunmadan yoluna devam ettiriyoruz.
Interceptor'ın çalışması için onu AppModule'de kaydetmen gerekir:
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
HTTP_INTERCEPTORS bir InjectionToken; useClass kısmı da daha önce gördüğümüz provider tanımının aynısı. Farkı multi: true satırında. Bu ayar "bu token'a tek bir değer değil, bir liste bağlanıyor; benimkini de listeye ekle" demek. Unutursan Angular multi ve tekil provider'ları karıştıramayacağını söyleyen bir hatayla uygulamayı açmaz.
Interceptor ile Global Hata Yönetimi
Interceptor yalnızca giden isteğe değil, dönen cevaba da dokunabilir. 401 ya da 500 gibi hataları her service'te ayrı ayrı ele almak yerine tek bir yerde toplayalım:
import { Injectable } from '@angular/core';
import {
HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
localStorage.removeItem('token');
console.warn('Oturum süresi doldu, tekrar giriş yapılmalı.');
} else if (error.status >= 500) {
console.error('Sunucu tarafında bir sorun var.');
}
return throwError(error);
})
);
}
}
next.handle(request) cevabı temsil eden bir observable döndürür; biz de onun arkasına catchError takıyoruz. Gerçek bir uygulamada console satırlarının yerinde bir bildirim gösterilir ya da kullanıcı giriş sayfasına yönlendirilir. Sayfalar arası yönlendirmeyi ilerleyen yazılarda detaylandıracağız.
Bu interceptor'ı da providers dizisine aynı şekilde, ikinci satır olarak ekle. Sıra önemlidir: istekler interceptor'lardan dizideki sırayla geçer, cevaplar ise ters sırayla geri döner. İç içe geçmiş matruşka bebekleri gibi; en dıştaki ilk açılır, son kapanır.
API Adresini Environment Dosyasına Taşımak
Şu ana kadar adresi service'in içine sabit yazdık. Geliştirmede bir sunucuya, canlıda başka bir sunucuya gitmek istediğinde bu sabit adres başına dert olur. src/environments/environment.ts dosyasını aç:
export const environment = {
production: false,
apiUrl: 'https://jsonplaceholder.typicode.com'
};
environment.prod.ts dosyasında da aynı alanı canlı sunucunun adresiyle tanımla. Service'te sabit adresin yerine environment'ı kullan:
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class UserService {
private apiUrl = `${environment.apiUrl}/users`;
constructor(private http: HttpClient) { }
}
Import her zaman environment.ts dosyasından yapılır, .prod dosyasından asla. Production build sırasında Angular CLI bu dosyayı otomatik olarak prod versiyonuyla değiştirir. Bu değişimin nasıl ayarlandığını build konusunu işlerken detaylandıracağız.
Küçük Bir Deneme
Şimdi sıra sende. Bir PostService yaz ve HttpParams ile /posts?userId=1 adresinden ilk kullanıcının yazılarını çek. Başlıkları bir listede göster, her satırın yanına bir "Sil" butonu koy. Butona basıldığında delete isteğini gönder ve başarılı dönünce o yazıyı diziden çıkar.
Sonra bilerek boz. Adresi /postsx yap, catchError içinde gelen status değerine bak ve ekranda anlaşılır bir hata mesajı göster. Son olarak isteğin method ve url bilgilerini konsola yazan küçük bir LoggingInterceptor yaz, HTTP_INTERCEPTORS ile kaydet ve sayfayı yenileyip konsolu izle.
Bu noktaya kadar her şey tek bir ekranda olup bitti. Kullanıcı listesinden birine tıklayıp onun detay sayfasına gitmek ya da adres çubuğundaki URL'e göre bambaşka bir ekran göstermek istediğinde işin içine router giriyor. Bir sonraki yazıda Angular'da routing'i, yani tek sayfalık bir uygulamada sayfalar arasında nasıl gezindiğimizi konuşacağız.