HTTP Client — Angular

HttpClient adalah cara Angular melakukan HTTP request. Setup import { provideHttpClient } from "@angular/common/http" // di app.config.ts export const…

HttpClient adalah service bawaan Angular untuk mengirim request HTTP (GET, POST, PUT, DELETE) ke server dan membaca jawabannya. Bayangin pelayan di warung Martabak Bang Jali. Kamu menulis pesanan di kertas, tapi pelayannya baru jalan ke dapur saat kamu menyerahkan kertas itu. Serahkan dua kertas, dia jalan dua kali. http.get() menulis kertasnya (sebuah Observable), dan subscribe() yang menyerahkannya.

🧪 Tebakan cepat: Constructor MenuPage menulis this.api.ambilMenu(), tanpa .subscribe(). Berapa request GET /api/menu yang terkirim ke server? Jawabannya ada di bagian "Belum subscribe, belum ada request".

Siapkan HttpClient

Di proyek Angular CLI, HttpClient didaftarkan sekali di app.config.ts:

import { ApplicationConfig } from '@angular/core'
import { provideHttpClient } from '@angular/common/http'

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

Lupa baris ini, dan service yang meminta HttpClient gagal dibuat dengan NG0201 (No provider found for ...) yang menyebut HttpClient; di Angular 18 namanya NullInjectorError. Editor latihan dan Playground menambahkannya sendiri begitu kodenya menyebut HttpClient.

GET dari service

Request ditaruh di service, seperti keranjang di pelajaran Services, supaya halaman mana pun bisa memakainya:

// src/app/menu-api.service.ts
import { Injectable, inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'

export interface Menu {
  id: number
  nama: string
  harga: number
  stok: number
}

@Injectable({ providedIn: 'root' })
export class MenuApi {
  private http = inject(HttpClient)

  ambilMenu(): Observable<Menu[]> {
    return this.http.get<Menu[]>('/api/menu')
  }
}

Server menjawab dengan JSON, dan HttpClient mengubahnya jadi array JavaScript:

[
  { "id": 1, "nama": "Martabak Manis Cokelat Keju", "harga": 40000, "stok": 4 },
  { "id": 2, "nama": "Martabak Telur Spesial", "harga": 50000, "stok": 6 },
  { "id": 3, "nama": "Terang Bulan Pandan", "harga": 35000, "stok": 0 }
]

Menampilkan: loading, data, error

// src/app/menu/menu.ts
import { Component, inject, signal } from '@angular/core'
import { HttpErrorResponse } from '@angular/common/http'
import { MenuApi, type Menu } from '../menu-api.service'

@Component({
  selector: 'app-menu',
  standalone: true,
  template: `
    <button (click)="muat()">Muat ulang</button>
    @if (loading()) {
      <p>Memuat menu...</p>
    } @else if (error()) {
      <p>Gagal: {{ error() }}</p>
    } @else {
      @for (m of menu(); track m.id) {
        <p>{{ m.nama }}: {{ m.harga }}</p>
      } @empty {
        <p>Menu belum tersedia.</p>
      }
    }
  `,
})
export class MenuPage {
  private api = inject(MenuApi)
  menu = signal<Menu[]>([])
  loading = signal(true)
  error = signal('')

  constructor() {
    this.muat()
  }

  muat() {
    this.loading.set(true)
    this.error.set('')
    this.api.ambilMenu().subscribe({
      next: (data) => {
        this.menu.set(data)
        this.loading.set(false)
      },
      error: (err: HttpErrorResponse) => {
        this.error.set(err.message)
        this.loading.set(false)
      },
    })
  }
}
Saat Yang tampil
Request baru terkirim Memuat menu...
Server menjawab 200 tiga baris, mulai dari Martabak Manis Cokelat Keju: 40000
Server gak bisa dihubungi Gagal: Http failure response for /api/menu: 0 Unknown Error

Belum subscribe, belum ada request

Observable dari HttpClient itu dingin (cold): get() cuma menyiapkan request. Tanpa subscribe(), 0 request terkirim dan halamannya tertahan di "Memuat menu...". Itu jawaban tebakannya.

Sebaliknya, tiap subscribe() mengirim request baru. Dua subscribe() pada ambilMenu(), misalnya satu untuk daftar dan satu lagi untuk menghitung jumlah menu, berarti 2 request yang sama ke server. {{ (menu$ | async)?.length }} yang ditulis dua kali di template juga dua request. Ambil sekali, simpan hasilnya di signal, lalu baca signal itu di mana pun.

Kenapa Observable, bukan Promise? Karena bisa dibatalkan: kalau langganannya dihentikan sebelum server menjawab (misalnya async pipe saat pindah halaman), HttpClient membatalkan request-nya. Operator RxJS (map, catchError, retry) juga bisa dipasang di pipe()-nya.

catchError: cadangan saat gagal

Kalau gagal cukup diganti daftar kosong, pasang catchError di service:

import { catchError, of } from 'rxjs'

ambilMenu(): Observable<Menu[]> {
  return this.http.get<Menu[]>('/api/menu').pipe(
    catchError(() => of([])),
  )
}

Saat server mati, catchError menangkap error-nya dan mengirim [] lewat next. Komponennya menerima daftar kosong, jadi @empty menampilkan Menu belum tersedia., error di subscribe gak pernah dipanggil, dan gak ada pesan gagal. Cocok untuk bagian sampingan; untuk halaman utama, biarkan error-nya sampai supaya pembeli tahu dan bisa Muat ulang.

Request lain bentuknya sama, dan juga baru terkirim saat di-subscribe: this.http.post('/api/pesanan', { menuId: 2, jumlah: 1 }).subscribe().

Latihan memakai pola service yang sama: ApiService dengan private http = inject(HttpClient) dan getData() yang mengembalikan this.http.get<any[]>('/api/data'), lalu komponen dengan private api = inject(ApiService) dan <p>Items: {{ items().length }}</p>.

⚠️ Jebakan yang sering ketemu

TL;DR: HttpClient (didaftarkan dengan provideHttpClient()) mengirim request HTTP; taruh di service dengan inject(HttpClient) dan this.http.get<Menu[]>('/api/menu'). Hasilnya Observable dingin: tanpa subscribe() gak ada request, dan tiap subscribe() mengirim request baru. Tampilkan tiga keadaan dengan signal loading, menu, dan error, dan matikan loading di next maupun error, bukan di complete. catchError(() => of([])) mengganti kegagalan dengan cadangan. HttpClient memakai Observable karena request-nya bisa dibatalkan dan bisa diolah operator RxJS.

Yang akan kamu pelajari