Jika Anda melihat Route Handler Next.js terpanggil dua kali saat pengguna hanya sekali klik, masalahnya sering bukan satu penyebab tunggal. Dalam praktik, request ganda bisa datang dari kombinasi prefetch Link, fetch yang dipicu ulang di client, Strict Mode saat development, atau desain API yang memakai GET untuk aksi yang memiliki side effect.
Gejalanya biasanya jelas tetapi membingungkan: log endpoint muncul dobel, stok berkurang dua kali, counter melonjak dua kali, webhook internal tertembak ulang, atau bug hanya muncul di local/dev dan sulit direproduksi di production. Kunci debugging-nya adalah membuktikan sumber request, bukan menebak. Anda perlu mengaitkan request dengan request-id, membandingkan header, memisahkan perilaku dev vs production, dan mengaudit siapa yang memanggil endpoint itu.
Mengenali gejala nyata di aplikasi Next.js
Kasus ini sering muncul dalam pola seperti berikut:
- Endpoint internal
/api/reserveatau/app/api/reserve/route.tstercatat dua kali dalam log. - Halaman produk dibuka, lalu stok atau kuota berubah meski pengguna belum menyelesaikan aksi apa pun.
- Navigasi ke halaman tertentu memicu request lebih awal, sebelum tombol ditekan.
- Bug muncul di development, tetapi saat diuji di production hasilnya tidak konsisten.
- Request kedua memiliki URL sama, namun header atau waktu pemanggilannya sedikit berbeda.
Masalah menjadi lebih serius jika endpoint tersebut mengubah state, misalnya:
- mengurangi stok,
- membuat draft order,
- menambah view/counter,
- mencatat audit log penting,
- mengirim event ke service lain.
Aturan praktis: jika sebuah endpoint menyebabkan perubahan data, jangan asumsikan endpoint itu aman dipanggil lebih dari sekali. Desainlah seolah-olah duplikasi request pasti akan terjadi.
Root cause yang paling sering
1. Prefetch dari Link memicu request lebih awal
Next.js dapat melakukan prefetch untuk mempercepat navigasi. Ini umumnya baik untuk performa, tetapi bisa mengejutkan jika halaman tujuan atau komponen di dalamnya langsung memanggil Route Handler internal yang memiliki efek samping.
Masalahnya bukan selalu karena <Link> langsung memanggil API. Yang sering terjadi adalah:
- Link melakukan prefetch terhadap route halaman.
- Halaman tujuan atau komponennya melakukan
fetchke endpoint internal. - Saat navigasi benar-benar terjadi, request lain bisa dipicu lagi tergantung pola rendering, cache, atau trigger client.
Jika endpoint Anda memakai GET untuk aksi seperti “reserve item” atau “increment counter”, prefetch dapat membuat side effect terjadi sebelum pengguna benar-benar bernavigasi atau berinteraksi.
2. Fetch client dipicu ulang karena lifecycle komponen
Di sisi client, request ganda sering datang dari komponen yang melakukan fetch dalam useEffect, lalu tanpa sengaja terpanggil ulang karena:
- dependency array berubah,
- state update memicu render ulang,
- komponen di-remount,
- efek development di Strict Mode.
Contoh pola rawan:
useEffect(() => {
fetch('/api/increment-view');
}, [productId, filters]);Jika filters berubah referensinya pada setiap render, efek ini bisa berjalan lagi meski secara logika pengguna tidak melakukan aksi baru.
3. React Strict Mode di development
Pada development, Strict Mode dapat membantu mendeteksi side effect yang tidak aman dengan menjalankan sebagian alur lebih dari sekali. Ini sering membuat developer menyimpulkan bahwa Next.js atau API mereka “bug”, padahal yang terlihat adalah perilaku development untuk menemukan masalah desain.
Karena itu, Anda harus selalu menjawab dua pertanyaan terpisah:
- Apakah request ganda hanya terjadi di development?
- Apakah side effect tetap mungkin terjadi ganda di production karena retry, race condition, atau trigger dobel dari client?
Kalau hanya muncul di dev, penyebabnya mungkin lebih dekat ke Strict Mode atau tooling pengembangan. Kalau tetap bisa terjadi di production, endpoint Anda memang perlu dibuat idempoten atau diubah desainnya.
4. Cache atau no-store digunakan tanpa memahami efeknya
Penggunaan cache di Next.js dan fetch perlu dipahami dengan hati-hati. Konfigurasi seperti no-store atau pemanggilan dinamis dapat membuat request benar-benar dikirim ulang, bukan memakai hasil yang sudah tersedia. Sebaliknya, asumsi bahwa request akan selalu dideduplikasi juga berbahaya jika pemanggilnya berbeda konteks atau berbeda fase render.
Intinya, jangan menjadikan cache sebagai mekanisme keselamatan untuk side effect. Cache adalah alat performa dan konsistensi baca, bukan jaminan “aksi hanya berjalan sekali”.
5. Menggunakan GET untuk aksi yang tidak idempoten
Ini adalah akar masalah yang paling penting. Metode GET seharusnya aman untuk dibaca ulang, diprefetch, dicache, atau dipanggil lebih dari sekali tanpa mengubah state. Jika Anda memakai GET untuk:
- mengurangi stok,
- membuat token sekali pakai,
- menambah counter bisnis penting,
- mencatat transaksi,
maka request ganda bukan sekadar bug implementasi, tetapi juga masalah kontrak HTTP dan desain backend.
Langkah investigasi yang terurut
Debugging akan jauh lebih cepat jika Anda tidak langsung mengubah kode, melainkan mengumpulkan bukti dulu.
1. Tambahkan request-id dan log yang bisa dikorelasikan
Mulailah dengan memberi identitas unik pada tiap request. Jika client memanggil endpoint, kirim header seperti x-request-id. Jika tidak ada, buat di server dan log semua konteks penting.
import { NextRequest, NextResponse } from 'next/server';
import { randomUUID } from 'crypto';
export async function GET(req: NextRequest) {
const requestId = req.headers.get('x-request-id') ?? randomUUID();
console.log(JSON.stringify({
requestId,
method: req.method,
pathname: req.nextUrl.pathname,
search: req.nextUrl.search,
userAgent: req.headers.get('user-agent'),
purpose: req.headers.get('purpose'),
secFetchMode: req.headers.get('sec-fetch-mode'),
secFetchDest: req.headers.get('sec-fetch-dest'),
referer: req.headers.get('referer'),
timestamp: new Date().toISOString()
}));
return NextResponse.json({ ok: true, requestId });
}Dengan log seperti ini, Anda bisa menjawab:
- Apakah benar ada dua request HTTP terpisah?
- Apakah request datang dari browser, server, atau proses lain?
- Apakah request pertama tampak seperti prefetch atau navigasi awal?
- Apakah kedua request punya header, referer, atau timing yang berbeda?
2. Inspeksi header dan pola waktu request
Header tertentu dapat memberi petunjuk, walau tidak selalu identik di semua browser atau runtime. Perhatikan terutama:
referer,purposejika ada,sec-fetch-*,user-agent,- cookie atau auth header,
- waktu antar request.
Jika request pertama datang sangat cepat saat link muncul di viewport atau saat halaman belum benar-benar dibuka, itu indikasi kuat adanya prefetch atau render awal. Jika request kedua muncul tepat setelah hydration atau state update di client, kemungkinan besar ada trigger tambahan dari komponen React.
3. Bedakan development dan production
Jangan campur hasil observasi dari dua environment ini. Uji keduanya secara terpisah:
- jalankan di development dan catat perilaku,
- buat reproduksi yang sama pada build production lokal,
- bandingkan jumlah request, urutan, dan header.
Jika bug hanya ada di development, peluang besar penyebabnya adalah Strict Mode atau alur render pengembangan. Jika tetap muncul di production, fokus pada desain endpoint, trigger client, dan idempotensi backend.
4. Audit semua pemicu di client dan server
Jangan hanya mencari satu fetch('/api/endpoint'). Audit semua titik yang mungkin memicu request:
useEffectpada komponen client,- event handler seperti
onClickdanonSubmit, - server component yang melakukan
fetch, - layout dan page yang sama-sama memanggil endpoint,
- prefetch dari navigasi,
- retry manual atau library data fetching,
- redirect yang menyebabkan alur dijalankan ulang.
Kesalahan umum adalah satu endpoint dipanggil oleh server component untuk menyiapkan data, lalu dipanggil lagi di client setelah hydration “untuk memastikan data terbaru”. Jika endpoint itu punya side effect, hasilnya akan berbahaya.
5. Buat reproduksi minimal
Hilangkan kompleksitas sampai tersisa skenario terkecil yang masih memunculkan bug:
- satu halaman,
- satu Link,
- satu Route Handler,
- satu komponen client jika perlu.
Contoh struktur minimal:
// app/products/page.tsx
import Link from 'next/link';
export default function Page() {
return <Link href="/checkout">Checkout</Link>;
}
// app/checkout/page.tsx
'use client';
import { useEffect } from 'react';
export default function CheckoutPage() {
useEffect(() => {
fetch('/api/reserve');
}, []);
return <div>Checkout</div>;
}
// app/api/reserve/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
console.log('reserve called');
return NextResponse.json({ ok: true });
}Contoh di atas sengaja menunjukkan desain yang rawan: route GET dipakai untuk reservasi. Jika di sini gejala muncul, Anda sudah punya bukti yang mudah dianalisis tanpa gangguan logika bisnis lain.
Perbaikan konkret yang paling efektif
1. Ubah aksi berside effect ke POST
Jika endpoint mengubah state, gunakan POST atau metode non-GET lain yang sesuai. Ini bukan hanya soal konvensi, tetapi cara memisahkan operasi baca dari operasi tulis agar prefetch, cache, dan pemanggilan ulang tidak merusak data bisnis.
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const body = await req.json();
// lakukan reservasi / increment / create draft order
// di sini, bukan di GET
return NextResponse.json({ ok: true, itemId: body.itemId });
}Lalu panggil hanya saat aksi pengguna benar-benar terjadi, misalnya pada submit atau klik tombol konfirmasi.
2. Tambahkan idempotency key untuk operasi penting
Untuk aksi seperti pembayaran, reservasi, pembuatan order, atau perubahan stok, idempotency key adalah lapisan perlindungan yang jauh lebih kuat daripada berharap request hanya terkirim sekali.
Alurnya:
- client membuat key unik per aksi,
- key dikirim ke server,
- server menyimpan hasil atau status berdasarkan key itu,
- jika request sama datang lagi, server mengembalikan hasil lama atau menolak duplikasi.
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const idemKey = req.headers.get('idempotency-key');
if (!idemKey) {
return NextResponse.json({ error: 'Missing idempotency key' }, { status: 400 });
}
// pseudo-code:
// 1. cek apakah idemKey sudah pernah diproses
// 2. jika ya, kembalikan hasil sebelumnya
// 3. jika belum, proses dalam transaksi dan simpan hasilnya
return NextResponse.json({ ok: true });
}Trade-off-nya adalah Anda perlu penyimpanan tambahan dan aturan masa berlaku key. Namun untuk endpoint kritis, ini hampir selalu sepadan.
3. Tambahkan guard deduplikasi di backend
Selain idempotency key, Anda bisa menerapkan guard berbasis data bisnis:
- unique constraint di database,
- cek status order sebelum mengubah stok,
- optimistic locking atau version check,
- transaksi database agar operasi tidak terpecah.
Contohnya, jika satu user hanya boleh memiliki satu reservasi aktif untuk item tertentu, pastikan aturan itu ditegakkan di database, bukan hanya di UI.
4. Kontrol prefetch jika memang mengganggu
Jika investigasi membuktikan prefetch membuat alur terlalu agresif, kendalikan perilakunya. Pendekatan paling aman adalah jangan menaruh side effect pada fase data loading yang bisa terjadi sebelum aksi pengguna. Namun pada kasus tertentu Anda juga bisa meminimalkan trigger navigasi dini pada elemen Link yang sensitif.
Yang perlu diingat, mematikan atau mengurangi prefetch hanya mengurangi satu sumber gejala. Itu bukan pengganti untuk memperbaiki desain endpoint yang tidak idempoten.
5. Pisahkan endpoint baca dan endpoint aksi
Pola yang lebih sehat adalah:
GET /api/product/123untuk membaca data, aman diprefetch atau dicache.POST /api/reservationsuntuk membuat reservasi.
Dengan pemisahan ini, Anda bisa tetap memanfaatkan performa framework tanpa mempertaruhkan konsistensi data.
Contoh perbaikan dari desain yang rawan ke desain yang aman
Desain rawan
// GET dipakai untuk side effect
export async function GET(req: NextRequest) {
const itemId = req.nextUrl.searchParams.get('itemId');
// kurangi stok di database
return NextResponse.json({ ok: true });
}Masalah:
- bisa terpanggil oleh prefetch,
- aman secara URL tetapi tidak aman secara bisnis,
- sulit dibedakan antara baca dan aksi.
Desain yang lebih aman
// Route Handler untuk aksi eksplisit
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const requestId = req.headers.get('x-request-id');
const idemKey = req.headers.get('idempotency-key');
const { itemId, quantity } = await req.json();
if (!idemKey) {
return NextResponse.json({ error: 'Missing idempotency key' }, { status: 400 });
}
// pseudo-code transaksi:
// - cek idemKey
// - cek stok masih cukup
// - buat reservasi
// - simpan hasil untuk idemKey
return NextResponse.json({ ok: true, requestId, itemId, quantity });
}// Client: panggil hanya pada aksi pengguna
const handleReserve = async () => {
const idemKey = crypto.randomUUID();
const requestId = crypto.randomUUID();
await fetch('/api/reservations', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-request-id': requestId,
'idempotency-key': idemKey,
},
body: JSON.stringify({ itemId: 'sku-123', quantity: 1 })
});
};Kenapa ini lebih aman:
- aksi hanya terjadi saat event eksplisit,
- duplikasi request lebih mudah dideteksi,
- kontrak HTTP sesuai dengan semantik operasinya,
- backend punya ruang untuk menolak atau menggabungkan request ganda.
Kesalahan umum saat menangani bug ini
- Menyalahkan Next.js terlalu cepat. Sering kali masalah ada pada desain endpoint atau lifecycle komponen.
- Memperbaiki hanya dengan debounce di client. Ini membantu UX, tetapi tidak cukup untuk melindungi backend.
- Menganggap production aman hanya karena dev dobel. Request ganda tetap bisa terjadi karena retry, klik dobel, race condition, atau jaringan.
- Memakai GET untuk write operation. Ini akar masalah yang paling mahal jika dibiarkan.
- Tidak punya korelasi log. Tanpa request-id, Anda mudah salah mengira dua log berbeda sebagai satu bug yang sama.
Checklist pencegahan
- Pastikan endpoint dengan side effect tidak memakai
GET. - Tambahkan
x-request-iduntuk korelasi log end-to-end. - Terapkan idempotency key pada operasi kritis.
- Gunakan transaksi database atau unique constraint untuk mencegah duplikasi data.
- Audit semua
useEffect, handler klik, dan pemanggilanfetchyang bisa berjalan ulang. - Uji skenario yang sama di development dan build production.
- Jangan mengandalkan cache atau prefetch behavior sebagai proteksi side effect.
- Pisahkan endpoint baca dan endpoint aksi.
- Buat reproduksi minimal sebelum mengubah banyak bagian sekaligus.
Penutup
Debug request ganda dari prefetch ke Route Handler di Next.js bukan sekadar mencari “kenapa endpoint dipanggil dua kali”, tetapi memastikan bahwa sistem Anda tetap benar meski request memang bisa datang lebih dari sekali. Prefetch, Strict Mode, render ulang client, dan cache hanyalah pemicu yang memperlihatkan kelemahan desain yang sudah ada.
Solusi yang paling tahan lama adalah kombinasi dari semantik HTTP yang benar, idempotensi backend, logging yang bisa dikorelasikan, dan audit trigger di client/server. Jika endpoint Anda aman terhadap duplikasi, maka prefetch atau request ulang tidak lagi menjadi ancaman terhadap stok, counter, atau integritas data bisnis.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!