Episode ini membahas integrasi API eksternal di Nuxt: memanggil layanan pihak ketiga dari server, melindungi request dengan auth headers, menyimpan secret dengan aman lewat runtime config, serta menerapkan rate limiting dan error handling yang tangguh.

Aplikasi nyata jarang bekerja sendiri — dia memanggil payment gateway, layanan email, atau API cuaca. Episode 13 membahas cara mengintegrasikan API eksternal dengan benar di Nuxt: dari mana harus memanggil, bagaimana request diamankan, di mana secret disimpan, dan bagaimana menangani kegagalan.
Aturan terpenting episode ini: panggilan API eksternal yang membutuhkan secret harus dilakukan di server, bukan di browser. Kode client bisa dibaca siapa saja, jadi api key dan token hanya boleh hidup di sisi server.
Tempat paling tepat untuk panggilan eksternal adalah server/api. Mari buat endpoint yang menghubungkan ke payment gateway:
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const hasil = await $fetch("https://api.payment.dev/v1/charge", {
method: "POST",
body,
headers: {
Authorization: `Bearer ${useRuntimeConfig().apiKeyPembayaran}`,
},
})
return hasil
})$fetch di server route membawa Authorization header yang diambil dari runtime config. Browser tidak pernah melihat api key ini karena seluruh panggilan terjadi di server.
Sering kali masalahnya bukan keamanan, tapi CORS — browser memblokir panggilan lintas origin. Memakai server route sebagai proksi menyelesaikan dua masalah sekaligus: CORS dan perlindungan secret.
Setiap request ke API eksternal yang butuh otorisasi wajib membawa token. Simpan token di runtime config dan jangan pernah hardcode:
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const res = await $fetch("https://api.eksternal.dev/v2/produk", {
headers: {
"X-Api-Key": config.apiKeyEksternal,
},
})
return res
})config.apiKeyEksternal dibaca dari NUXT_API_KEY_EKSTERNAL di environment. Header kustom seperti X-Api-Key adalah pola umum yang dipakai banyak API pihak ketiga.
Sebagian API publik memang tidak butuh token, tapi tetap batasi pemanggilannya. Jika API lain berbayar atau punya kuota, jangan membiarkan siapa pun memanggilnya tanpa kendali.
Klasifikasi data itu krusial. Yang boleh di runtimeConfig.public hanya data publik seperti base URL. Api key, token, dan password hanya di runtimeConfig private:
export default defineNuxtConfig({
runtimeConfig: {
stripeSecret: process.env.STRIPE_SECRET_KEY,
public: {
stripePublishable: process.env.STRIPE_PUBLISHABLE_KEY,
},
},
})stripeSecret tidak pernah dikirim ke client, sedangkan stripePublishable aman dipakai di frontend — misalnya untuk menampilkan form kartu.
Kesalahan umum: console.log(config.stripeSecret) saat debugging, lalu bocor ke log server yang bisa dibaca orang lain. Buat aturan sederhana: jangan pernah menampilkan secret di log, error message, atau response API.
Rate limiting membatasi jumlah request per pengguna. Nitro menyediakan storage yang cocok untuk pola ini:
export default defineEventHandler(async (event) => {
const ip = getRequestIP(event, { xForwardedFor: true })
const kunci = `rate:${ip}`
const storage = useStorage("cache")
const jumlah = (await storage.getItem(kunci)) || 0
if (jumlah >= 100) {
throw createError({
statusCode: 429,
message: "Terlalu banyak request",
})
}
await storage.setItem(kunci, jumlah + 1, { ttl: 3600 })
return { ok: true }
})Pola di atas menghitung request per IP memakai storage Nitro dengan TTL satu jam. Status 429 dikembalikan saat batas terlampaui — melindungi endpoint dari brute force dan penyalahgunaan.
Sesuaikan batas dengan sifat endpoint: halaman publik bisa lebih longgar, endpoint login sebaiknya ketat. Kombinasikan dengan backoff eksponensial di sisi client untuk pengalaman yang tetap baik.
API pihak ketiga bisa gagal, lambat, atau mengubah format. Server route wajib menangkap error dan mengembalikan pesan yang jelas:
export default defineEventHandler(async () => {
try {
const res = await $fetch("https://api.eksternal.dev/v1/status")
return res
} catch (e) {
throw createError({
statusCode: 503,
message: "Layanan eksternal sedang tidak tersedia",
})
}
})createError({ statusCode: 503, ... }) menyampaikan kegagalan ke client dengan status yang tepat. Jangan pernah membocorkan detail internal seperti stack trace ke pengguna.
Untuk kegagalan yang bersifat sementara, tambahkan retry dengan jeda yang meningkat:
for (let i = 1; i <= 3; i++) {
try {
return await $fetch("https://api.eksternal.dev/v1/resi")
} catch (e) {
if (i === 3) throw e
await new Promise((r) => setTimeout(r, i * 500))
}
}Pola retry di atas mencoba hingga tiga kali dengan jeda bertingkat. Kombinasikan dengan timeout agar request yang menggantung tidak menghabiskan resource server.
Episode 13 mengamankan integrasi eksternal kalian: panggilan API pihak ketiga dilakukan dari server, request dilindungi auth headers, secret disimpan hanya di sisi server lewat runtime config, rate limiting melindungi endpoint dari penyalahgunaan, dan error handling membuat sistem tangguh terhadap kegagalan.
Inti yang harus dibawa pulang:
runtimeConfig private, tidak pernah di public.Di episode 14 selanjutnya kita akan membahas caching dan performance — caching data dengan Nitro dan browser cache, static generation dan ISR, optimasi page load dan pengiriman aset, serta pemantauan build performance. Toko kalian mulai berlari kencang.