Mengambil data dari API di Astro
Satu fetch tanpa timeout di frontmatter adalah satu-satunya hal yang dibutuhkan untuk membuat portalmu tergantung pada uptime pihak lain.
Intisari
fetchtersedia di frontmatter tanpa impor. Ia berjalan di server, bukan di browser.fetchtidak punya timeout default. TanpaAbortSignal, halamanmu menunggu selamanya.- Beberapa panggilan independen harus
Promise.allโ berurutan berarti menjumlahkan latensinya. - Setiap API luar butuh jawaban untuk: apa yang tampil kalau ia mati? Kalau jawabannya "halaman error", rancangannya salah.
- Panggilan yang gagal harus mengurangi TTL cache, bukan menyimpan kegagalan itu di edge selama lima menit.
Bentuk paling sederhana, dan kenapa ia berbahaya
---
// JANGAN begini di produksi
const res = await fetch("https://api-data.internal/perusahaan/BBCA");
const perusahaan = await res.json();
---
Tiga hal salah sekaligus:
- Tidak ada timeout.
fetchdi Node akan menunggu selama koneksi terbuka. API yang menggantung berarti halamanmu menggantung โ dan tiap request yang menggantung memegang satu koneksi ALB dan satu slot di event loop. - Tidak ada pemeriksaan status.
fetchhanya melempar untuk kegagalan jaringan. Respons 500 dianggap sukses, lalu.json()yang meledak โ dengan pesan error tentang JSON, bukan tentang API yang mati. - Tidak ada validasi.
perusahaanbertipeany. Seluruh keuntungan TypeScript hilang di baris ini.
Bentuk yang layak produksi
// src/lib/api.ts
export class GalatApi extends Error {
constructor(pesan: string, readonly status?: number) {
super(pesan);
}
}
export async function ambilJson<T>(
url: string,
opsi: RequestInit & { timeoutMs?: number } = {},
): Promise<T> {
const { timeoutMs = 3000, ...sisa } = opsi;
const res = await fetch(url, {
...sisa,
signal: AbortSignal.timeout(timeoutMs),
headers: {
Accept: "application/json",
"User-Agent": "portal-berita/1.0",
...sisa.headers,
},
});
if (!res.ok) {
throw new GalatApi(`${url} menjawab ${res.status}`, res.status);
}
return res.json() as Promise<T>;
}
AbortSignal.timeout() tersedia di Node 22 tanpa dependensi apa pun. Tiga detik adalah titik
awal yang masuk akal untuk API internal; untuk API pihak ketiga yang memang lambat, naikkan โ tapi
selalu ada angkanya.
Timeout fetch-mu harus lebih kecil dari timeout halamanmu, yang lebih kecil dari timeout ALB. Aturan yang sama dengan kueri database di Fase 2: mengecil ke dalam. Kalau fetch-mu 30 detik sementara ALB menyerah di 60, pembaca sudah pergi jauh sebelum halamanmu selesai โ dan container-mu masih bekerja untuk halaman yang tidak akan pernah dilihat siapa pun.
Paralel, bukan berurutan
---
// BURUK: 120 ms + 90 ms + 150 ms = 360 ms
const berita = await ambilJson<Berita[]>(URL_BERITA);
const kurs = await ambilJson<Kurs>(URL_KURS);
const indeks = await ambilJson<Indeks>(URL_INDEKS);
---
---
// BAIK: yang terlama saja = 150 ms
const [berita, kurs, indeks] = await Promise.all([
ambilJson<Berita[]>(URL_BERITA),
ambilJson<Kurs>(URL_KURS),
ambilJson<Indeks>(URL_INDEKS),
]);
---
Ini kesalahan yang paling sering di beranda portal, karena kodenya dibaca dari atas ke bawah dan
await berurutan terasa alami. Setiap kali kamu menulis dua await berturut-turut,
tanyakan: apakah yang kedua butuh hasil yang pertama? Kalau tidak, gabungkan.
Kegagalan sebagian: allSettled
---
const [beritaHasil, kursHasil] = await Promise.allSettled([
ambilJson<Berita[]>(URL_BERITA),
ambilJson<Kurs>(URL_KURS),
]);
// Berita wajib โ tanpa ini halaman tidak ada gunanya
if (beritaHasil.status === "rejected") {
Astro.response.status = 503;
return Astro.rewrite("/gangguan");
}
const berita = beritaHasil.value;
// Kurs opsional โ widget-nya sembunyi kalau gagal
const kurs = kursHasil.status === "fulfilled" ? kursHasil.value : null;
// Jangan simpan kegagalan di edge selama lima menit.
const ttl = kurs ? 300 : 30;
Astro.response.headers.set("Cache-Control", `public, s-maxage=${ttl}`);
---
{kurs && <WidgetKurs data={kurs} />}
Baris const ttl = kurs ? 300 : 30 itu yang paling sering dilupakan. Kalau kamu
meng-cache halaman berisi kegagalan selama lima menit, maka satu kegagalan sesaat jadi lima menit
kerusakan yang terlihat semua pembaca โ bahkan setelah API-nya pulih. TTL harus mencerminkan seberapa
yakin kamu pada isinya.
Peta keputusan: setiap API luar butuh jawaban ini
| Pertanyaan | Contoh: API kurs | Contoh: API langganan |
|---|---|---|
| Halaman masih berguna tanpanya? | Ya | Tidak untuk halaman akun |
| Kalau gagal, tampilkan apa? | Sembunyikan widget | Pesan "cek lagi sebentar" |
| Boleh basi berapa lama? | 15 menit | Beberapa detik |
| Timeout | 2 detik | 3 detik |
| Di mana dipanggil | Frontmatter halaman | Server island |
Baris terakhir itu penting. Data yang bergantung pada siapa pembacanya tidak boleh diambil di frontmatter halaman yang di-cache โ hasilnya akan ikut tersimpan di edge dan tersaji ke orang lain. Tempatnya di server island (Fase 1).
Jangan panggil API-mu sendiri lewat HTTP
---
// BURUK: halaman memanggil endpoint-nya sendiri
const res = await fetch(new URL("/api/artikel.json", Astro.url));
---
Ini menambah satu round-trip HTTP penuh โ lewat loopback, lewat middleware lagi, dan menghabiskan satu koneksi server untuk melayani servermu sendiri. Di trafikmu itu menggandakan jumlah request yang harus ditangani Node. Panggil fungsinya langsung:
---
// BAIK
import { ambilArtikelTerbaru } from "@/lib/artikel";
const artikel = await ambilArtikelTerbaru("semua", 20);
---
Endpoint /api/artikel.json tetap ada untuk konsumen luar. Halamanmu sendiri memakai fungsi
yang sama yang dipakai endpoint itu โ satu sumber logika, nol hop tambahan.
Latihan: tulis ambilJson dengan timeout, lalu bangun halaman yang mengambil dua
sumber dengan Promise.allSettled โ satu wajib, satu opsional. Uji dengan API palsu yang
sengaja tidur 10 detik dan pastikan halamanmu tetap merender dalam 3 detik dengan widget opsional
tersembunyi. Verifikasi juga bahwa Cache-Control yang keluar memakai TTL pendek saat itu
terjadi.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.