Debugging backend saat endpoint dipaksa mirip aplikasi native biasanya bermula dari keputusan produk yang terlihat kecil: halaman yang awalnya bisa dirender sebagai satu alur web justru dipecah menjadi banyak panggilan API kecil. Hasilnya bukan sekadar lebih banyak endpoint, tetapi backend menjadi chatty, state tersebar, dan kegagalan kecil di satu request bisa merusak keseluruhan alur pengguna.

Studi kasus di artikel ini membahas pola yang sering muncul di tim backend: form, dashboard, atau detail halaman diperlakukan seolah klien adalah aplikasi native penuh, padahal kebutuhan dasarnya masih cocok disajikan sebagai webpage atau minimal endpoint agregasi yang lebih kasar. Gejalanya biasanya jelas: waterfall request panjang, refresh token saling balapan, payload yang saling tumpang tindih, dan lonjakan 429 atau 5xx saat trafik naik sedikit saja.

Latar sistem: dari halaman sederhana menjadi API yang terlalu granular

Bayangkan sistem internal untuk portal pelanggan. Awalnya, halaman Account Overview hanya butuh tiga blok data:

  • profil pengguna,
  • status langganan,
  • 10 transaksi terakhir.

Dalam model webpage tradisional, ini bisa dilayani oleh satu request server-side yang mengumpulkan data dan mengembalikan satu halaman. Namun tim memutuskan menyamakan pola dengan aplikasi mobile, sehingga frontend web memanggil banyak endpoint terpisah:

  • GET /api/me
  • GET /api/subscription
  • GET /api/transactions?limit=10
  • GET /api/notifications/unread-count
  • GET /api/feature-flags
  • POST /api/session/refresh bila token hampir kedaluwarsa

Masalahnya bukan pada keberadaan API terpisah itu sendiri, melainkan pada cara penggunaannya. Web client memanggil semuanya hampir bersamaan setiap kali halaman dimuat, lalu komponen lain melakukan panggilan tambahan ketika sebagian data sudah masuk. Di atas kertas tampak modular. Di produksi, ini menciptakan banyak titik gagal.

Gejala yang muncul di produksi

1. Request berantai berlebihan

Dari browser trace terlihat satu kunjungan halaman memicu 12-25 request API. Sebagian request saling bergantung secara implisit. Misalnya, setelah /api/me selesai, frontend baru memanggil /api/preferences dan /api/permissions. Ketika latensi salah satu request naik, seluruh halaman ikut terasa lambat.

2. Race condition pada session atau token

Beberapa komponen mendeteksi token hampir habis pada saat yang sama. Masing-masing memanggil endpoint refresh. Akibatnya:

  • token lama dibatalkan terlalu cepat,
  • request lain masih memakai token lama,
  • muncul 401 acak,
  • mekanisme retry memperparah beban.

3. Payload duplikat dan kueri berulang

Data yang sama dikirim dari beberapa endpoint berbeda. Contohnya, informasi pengguna muncul di /api/me, /api/subscription, dan /api/notifications karena setiap service ingin “lengkap”. Di backend, setiap endpoint juga memuat relasi yang serupa dari database atau service lain.

4. Lonjakan 429 dan 5xx

Ketika satu halaman memicu banyak request, pembatasan laju dan pool koneksi cepat tertekan. Bahkan bila satu pengguna hanya membuka satu halaman, backend melihat ledakan request kecil dalam waktu sangat singkat. Efek yang sering terlihat:

  • 429 Too Many Requests dari API gateway atau rate limiter,
  • 502/503/504 dari upstream yang kehabisan koneksi atau timeout,
  • 500 dari kode aplikasi yang tidak siap menghadapi retry paralel atau state session yang berubah.

Langkah investigasi: jangan mulai dari asumsi, mulai dari jejak request

1. Petakan satu user journey menjadi trace lengkap

Mulailah dari satu skenario konkret, misalnya “membuka halaman Account Overview setelah login”. Ambil:

  • HAR file dari browser,
  • access log backend,
  • trace APM atau distributed tracing bila tersedia,
  • log dari auth service dan cache.

Tujuannya bukan langsung mencari bug di kode, tetapi menjawab pertanyaan dasar:

  1. Berapa request yang benar-benar terjadi untuk satu load halaman?
  2. Mana yang paralel, mana yang berantai?
  3. Mana yang gagal duluan saat sistem mulai lambat?
  4. Adakah pola retry atau refresh token berulang?

2. Tambahkan correlation ID dan request group ID

Kalau log hanya berisi request individual, debugging akan lambat. Tambahkan minimal dua penanda:

  • Correlation ID untuk satu request end-to-end.
  • Page-load atau interaction ID untuk mengelompokkan semua request yang berasal dari satu aksi pengguna.

Ini penting karena bug pada arsitektur chatty sering baru terlihat ketika beberapa endpoint dianalisis sebagai satu grup, bukan terpisah.

X-Request-Id: req_9f3b2c1a
X-Interaction-Id: page_01jz8m4x7k

3. Ukur distribusi, bukan hanya rata-rata

Rata-rata latensi sering menipu. Satu endpoint yang rata-ratanya 80 ms bisa tetap berbahaya bila p95 atau p99 melonjak. Dalam arsitektur yang terlalu granular, latensi total halaman adalah gabungan dari banyak request. Sedikit degradasi di beberapa endpoint bisa menghasilkan pengalaman yang jauh lebih buruk daripada angka rata-ratanya.

4. Cari pola request identik yang terjadi hampir bersamaan

Dari log, kelompokkan berdasarkan:

  • path,
  • user ID atau session ID,
  • rentang waktu pendek, misalnya 1-3 detik,
  • status code.

Kalau terlihat /api/session/refresh dipanggil 3-5 kali oleh user yang sama dalam satu detik, hampir pasti ada race condition atau desain klien yang tidak terkoordinasi.

Temuan root cause pada studi kasus

Root cause 1: UI web diperlakukan seperti klien native tanpa kebutuhan yang sama

Pada aplikasi native, pemecahan endpoint kadang masuk akal karena layar, cache lokal, dan siklus hidup aplikasi berbeda. Namun pada webpage, satu route sering lebih efisien bila data utama digabung di server. Saat desain endpoint terlalu atomik, frontend web menciptakan waterfall request yang sebenarnya bisa dihindari.

Root cause 2: refresh token tidak diserialisasi

Semua komponen memakai interceptor yang sama: bila token dianggap hampir habis, mereka refresh sendiri-sendiri. Backend auth mengeluarkan token baru dan menandai token lama tidak valid. Request paralel yang terlambat menjadi gagal karena masih memakai token lama.

Root cause 3: endpoint “mandiri” memuat konteks yang sama berulang kali

Setiap endpoint melakukan lookup user, permission, subscription state, dan preferensi dengan kueri atau panggilan downstream terpisah. Secara fungsional benar, tetapi secara sistemik boros. Beban bukan hanya pada aplikasi utama, melainkan pada cache, database, dan service autentikasi.

Root cause 4: retry buta memperparah beban

Frontend mengulang request 429 dan 5xx tanpa pembatasan yang cukup. Saat backend mulai tertekan, retry paralel justru memperbesar antrean. Ini adalah feedback loop yang umum pada sistem chatty.

Perbaikan arsitektur: kurangi percakapan, gabungkan kebutuhan

1. Buat endpoint agregasi untuk kebutuhan halaman

Untuk webpage atau route dengan kebutuhan data yang relatif stabil, lebih baik sediakan endpoint agregasi atau render server-side daripada banyak endpoint kecil. Bukan berarti semua API harus menjadi monolitik, tetapi bentuk antarmuka harus mengikuti pola konsumsi.

Sebelum:

GET /api/me
GET /api/subscription
GET /api/transactions?limit=10
GET /api/notifications/unread-count
GET /api/feature-flags

Sesudah:

GET /api/account-overview

Contoh respons yang lebih terarah:

{
  "user": {
    "id": "u_123",
    "name": "Alya",
    "email": "[email protected]"
  },
  "subscription": {
    "plan": "pro",
    "status": "active"
  },
  "recentTransactions": [
    { "id": "tx_1", "amount": 125000, "status": "settled" }
  ],
  "notifications": {
    "unreadCount": 3
  },
  "featureFlags": {
    "newBillingPage": true
  }
}

Mengapa ini bekerja? Karena backend dapat:

  • mengambil data dalam satu konteks auth,
  • menghindari lookup user yang berulang,
  • mengelola fallback secara terpusat,
  • menerapkan cache yang lebih relevan terhadap halaman.

2. Pisahkan endpoint interaksi dari endpoint render-data

Masalah umum muncul ketika endpoint kecil dipakai untuk dua kebutuhan sekaligus: render halaman dan aksi bisnis. Pisahkan keduanya:

  • Endpoint agregasi atau render-data untuk memuat halaman.
  • Endpoint spesifik untuk aksi seperti update profil, bayar tagihan, atau tandai notifikasi dibaca.

Dengan begitu, granularitas tetap ada untuk operasi mutasi, tetapi halaman baca tidak dipaksa melalui banyak panggilan terpisah.

3. Terapkan single-flight untuk refresh token

Baik di klien maupun backend auth, refresh sebaiknya dikoordinasikan agar satu session tidak melakukan beberapa refresh bersamaan. Di sisi backend, Anda bisa menambahkan kunci singkat berbasis session ID untuk mencegah refresh ganda yang simultan.

function refreshSession(sessionId, refreshToken) {
  const lockKey = `session-refresh:${sessionId}`;

  if (!acquireShortLock(lockKey, 3000)) {
    return waitForCurrentRefreshResult(sessionId);
  }

  try {
    const session = validateRefreshToken(refreshToken);
    const newTokens = issueNewTokens(session.userId, sessionId);
    storeRefreshResult(sessionId, newTokens, 5000);
    return newTokens;
  } finally {
    releaseLock(lockKey);
  }
}

Implementasinya bisa memakai Redis, database advisory lock, atau mekanisme locking lain. Detail alat tidak sepenting prinsipnya: satu session, satu refresh aktif.

4. Kurangi payload duplikat melalui kontrak data yang jelas

Bila beberapa endpoint tetap dibutuhkan, hindari kebiasaan “sekalian kirim semua konteks”. Tentukan kontrak tiap endpoint dengan tegas:

  • apa yang wajib ada,
  • apa yang tidak boleh diulang,
  • bagaimana referensi silang dilakukan.

Misalnya, endpoint transaksi tidak perlu selalu mengirim profil pengguna lengkap bila halaman sudah mendapatkannya dari endpoint agregasi.

5. Gunakan cache secara selektif, bukan menutup desain buruk

Cache dapat membantu, tetapi jangan menjadikannya penutup untuk pola request yang salah. Cache paling efektif setelah jumlah request per halaman sudah masuk akal. Jika tidak, cache hanya mengurangi rasa sakit sementara sambil mempertahankan kompleksitas dan titik gagal yang sama.

Perbaikan kode dan perilaku operasional

1. Hindari retry otomatis tanpa batas konteks

Untuk 429 dan sebagian 5xx, retry harus:

  • dibatasi jumlahnya,
  • menggunakan backoff,
  • memperhatikan idempotensi,
  • menghormati header seperti Retry-After bila ada.

Retry buta pada request paralel sering lebih merusak daripada membantu.

2. Tambahkan deduplikasi request untuk operasi baca yang identik

Jika gateway atau backend menerima beberapa request identik dari user yang sama dalam jendela waktu sangat pendek, deduplikasi sementara bisa mengurangi beban. Ini berguna khususnya saat frontend belum sempat diperbaiki sepenuhnya.

3. Lindungi downstream dengan batas konkurensi

Bila endpoint agregasi harus memanggil beberapa service internal, batasi konkurensi dan terapkan timeout per downstream. Tanpa itu, endpoint agregasi hanya memindahkan masalah dari browser ke server.

async function getAccountOverview(ctx) {
  const user = await getUser(ctx.userId);

  const [subscription, transactions, notifications] = await Promise.all([
    withTimeout(() => getSubscription(user.id), 300),
    withTimeout(() => getRecentTransactions(user.id, 10), 500),
    withTimeout(() => getUnreadNotificationCount(user.id), 200)
  ]);

  return {
    user,
    subscription,
    recentTransactions: transactions,
    notifications
  };
}

Contoh di atas tetap perlu penanganan kesalahan parsial sesuai kebutuhan produk. Jangan memaksa semua downstream harus sukses jika halaman masih bisa ditampilkan dengan degradasi yang wajar.

4. Bedakan kegagalan parsial dan kegagalan total

Untuk halaman ringkasan, kegagalan menghitung jumlah notifikasi seharusnya tidak selalu membuat seluruh endpoint mengembalikan 500. Lebih aman mengembalikan data inti dan menandai bagian tertentu sebagai tidak tersedia.

Prinsip praktis: jangan biarkan data sekunder menjatuhkan respons primer, kecuali benar-benar mempengaruhi keputusan bisnis utama.

Logging dan metrik yang benar-benar membantu

Field log minimum

  • request_id
  • interaction_id
  • user_id atau session_id yang sudah disamarkan sesuai kebijakan
  • endpoint
  • status_code
  • duration_ms
  • downstream_calls
  • token_refresh_attempted
  • retry_count

Dengan field ini, Anda bisa menjawab apakah sebuah 500 berdiri sendiri atau bagian dari ledakan request yang lebih besar.

Metrik yang perlu dipantau

  • Requests per page-load: berapa banyak request backend yang lahir dari satu aksi pengguna.
  • Refresh token per session per menit: lonjakan kecil di sini sering menjadi sinyal race condition.
  • Duplicate request ratio: persentase request identik dari user yang sama dalam jendela pendek.
  • 429 rate per endpoint: untuk melihat endpoint mana yang paling tertekan.
  • p95/p99 latency per endpoint dan per downstream.
  • Error fan-out: satu kegagalan upstream memicu berapa banyak kegagalan turunan.

Contoh log yang berguna

{
  "request_id": "req_9f3b2c1a",
  "interaction_id": "page_01jz8m4x7k",
  "user_id": "u_123",
  "endpoint": "/api/session/refresh",
  "status_code": 200,
  "duration_ms": 84,
  "retry_count": 0,
  "token_refresh_attempted": true
}
{
  "request_id": "req_9f3b2c1b",
  "interaction_id": "page_01jz8m4x7k",
  "user_id": "u_123",
  "endpoint": "/api/session/refresh",
  "status_code": 409,
  "duration_ms": 12,
  "retry_count": 0,
  "token_refresh_attempted": true,
  "note": "refresh already in progress"
}

Log kedua penting karena memberi tahu bahwa konflik itu disengaja dan terkendali, bukan kegagalan acak.

Sebelum dan sesudah: perubahan endpoint yang berdampak

Sebelum

Pola lama:

  • Frontend memuat banyak komponen independen.
  • Setiap komponen memanggil endpoint sendiri.
  • Interceptor auth berlaku global tanpa koordinasi.
  • Retry aktif untuk hampir semua kegagalan.
GET /api/me
GET /api/preferences
GET /api/permissions
GET /api/subscription
GET /api/transactions?limit=10
GET /api/notifications/unread-count
POST /api/session/refresh

Sesudah

Pola yang lebih sehat:

  • Halaman utama memakai satu endpoint agregasi.
  • Refresh token diserialisasi.
  • Data sekunder boleh gagal secara parsial.
  • Retry dibatasi dan sadar konteks.
GET /api/account-overview
POST /api/profile
POST /api/notifications/mark-read
POST /api/session/refresh

Perubahan ini biasanya memberi dampak lebih besar daripada sekadar mengoptimalkan satu query lambat, karena akar masalahnya ada pada pola komunikasi.

Kesalahan umum saat memperbaiki masalah ini

  • Menambah cache di semua tempat tanpa mengurangi jumlah request.
  • Menganggap 429 adalah masalah konfigurasi rate limiter saja, padahal sumbernya adalah ledakan request per halaman.
  • Menggabungkan semua endpoint menjadi satu endpoint raksasa tanpa batas domain yang jelas.
  • Memperbaiki frontend saja tanpa memperkuat backend terhadap refresh ganda, retry, dan deduplikasi.
  • Melihat tiap endpoint secara terisolasi dan gagal memahami satu user journey utuh.

Checklist pencegahan untuk developer backend

  1. Ukur request per halaman atau per interaksi pengguna, bukan hanya per endpoint.
  2. Tanyakan apakah kebutuhan ini sebenarnya cocok sebagai webpage atau endpoint agregasi sebelum memecah data menjadi banyak API kecil.
  3. Pastikan refresh token atau session renewal tidak bisa balapan untuk session yang sama.
  4. Definisikan kontrak data agar payload tidak duplikatif antar endpoint.
  5. Batasi retry dan pastikan hanya dilakukan untuk kasus yang aman.
  6. Tambahkan correlation ID dan interaction ID sejak awal.
  7. Monitor 429, 5xx, duplicate request ratio, dan refresh rate per session.
  8. Rancang degradasi parsial untuk data sekunder pada halaman ringkasan.
  9. Lindungi downstream dengan timeout, batas konkurensi, dan fallback yang masuk akal.
  10. Audit endpoint yang dipakai frontend web: kalau sebuah halaman butuh terlalu banyak panggilan, itu sinyal desain perlu diubah.

Penutup

Debugging backend saat endpoint dipaksa mirip aplikasi native jarang selesai hanya dengan menambal satu bug. Biasanya ada masalah yang lebih mendasar: bentuk API tidak sesuai dengan cara data dikonsumsi oleh webpage. Akibatnya, backend menjadi terlalu banyak bicara, terlalu banyak menyimpan state implisit, dan terlalu mudah jatuh saat ada sedikit gangguan.

Perbaikan paling efektif biasanya datang dari kombinasi dua hal: menyederhanakan alur data di level arsitektur dan memperkeras backend terhadap balapan token, retry, serta ledakan request identik. Jika tim backend mulai mengukur beban per interaksi pengguna, bukan hanya per endpoint, akar masalah seperti ini akan jauh lebih cepat terlihat sebelum menjadi insiden produksi.