Implementasi Webhook di Node.js: Menerima dan Memproses Event Eksternal secara Real-Time
Mengapa Webhook Menjadi Standar Integrasi Event Real-Time
Webhook adalah mekanisme di mana sebuah server eksternal mengirim HTTP POST ke endpoint milik kita setiap kali terjadi suatu kejadian. Pola ini sering disebut sebagai reverse API karena arah komunikasinya terbalik. Pada API biasa, aplikasi kita yang bertanya dan server yang menjawab. Pada webhook, server pemberi event yang menghubungi aplikasi kita lebih dulu.
Pendekatan alternatif yang sering dibandingkan adalah polling. Dengan polling, aplikasi kita mengirim permintaan secara berulang dengan interval tertentu untuk mengecek apakah ada data baru. Cara ini boros resource, terutama ketika kejadian jarang terjadi. Server menerima banyak permintaan kosong, dan data tetap tertunda hingga interval berikutnya. Webhook mengatasi dua masalah ini sekaligus, data terkirim seketika dan tidak ada permintaan yang sia-sia.
Bayangkan notifikasi pembayaran dari payment gateway. Saat transaksi berhasil, gateway langsung mengirim payload ke endpoint kita dalam hitungan detik. Tanpa webhook, kita harus mengecek status transaksi setiap beberapa detik untuk semua pesanan yang berjalan. Kasus serupa terjadi pada event CI/CD ketika build selesai, dan pada aktivitas repository di GitHub seperti pull request baru.
Alur umum webhook cukup sederhana. Event source mendeteksi kejadian, lalu mengirim HTTP request ke receiver endpoint, dan endpoint memproses payload tersebut. Analoginya singkat: webhook seperti panggilan telepon dari server pemberi event ketika ada kabar baru, bukan kita yang terus-menerus menelepon untuk bertanya. Dalam artikel ini, kita akan membangun sisi receiver dari alur tersebut dengan Node.js.
Gambar: Alur penerimaan webhook pada sisi receiver, dari event source hingga worker asinkron — Sumber: Ilustrasi original untuk artikel ini
Membangun HTTP Endpoint untuk Menerima Payload Webhook
Kita mulai dengan membuat server Express minimal dan menambahkan middleware express.json() untuk parsing payload JSON. Middleware ini membaca body dari request dan mengubahnya menjadi objek JavaScript yang bisa diakses melalui req.body.
Response harus dikembalikan secepat mungkin. Pengirim webhook umumnya menunggu status 2xx sebagai tanda bahwa payload sudah diterima dengan baik. Jika response tidak tiba dalam batas waktu tertentu, pengirim akan menganggap delivery gagal dan melakukan retry. Artinya, pekerjaan berat tidak boleh dilakukan di dalam handler. Kita cukup mengakui penerimaan, lalu menunda processing ke tahap berikutnya.
Ada satu detail penting yang sering terlewat, yaitu menyimpan rawBody. Middleware express.json() mengubah body menjadi objek, tetapi verifikasi signature membutuhkan data mentah persis seperti yang dikirim. Kita bisa memanfaatkan opsi verify pada middleware untuk menangkap buffer asli sebelum parsing.
Struktur payload webhook biasanya berisi event type, event ID, timestamp, dan data. Event type mengidentifikasi jenis kejadian, event ID menjadi identitas unik, dan data membawa informasi detail yang dibutuhkan untuk processing.
const express = require('express');
const app = express();
app.post(
'/webhook',
express.json({
verify: (req, res, buffer) => {
req.rawBody = buffer;
},
}),
(req, res) => {
const { event, eventId, timestamp, data } = req.body;
console.log(`[webhook] Event diterima: ${event} (${eventId})`);
res.status(200).json({ received: true });
}
);
app.listen(3000, () => {
console.log('Webhook receiver berjalan di port 3000');
});Ketika payload tiba, handler mencatat event type dan event ID ke log, lalu langsung mengembalikan status 200. Log tersebut berguna sebagai jejak audit saat kita memeriksa alur event. Endpoint ini sudah siap menerima kiriman dari layanan eksternal, tetapi belum aman karena siapa pun yang mengetahui URL bisa mengirim payload palsu. Kita perlu menambahkan verifikasi signature.
Verifikasi Signature untuk Memastikan Request Berasal dari Sumber Terpercaya
Setiap layanan webhook besar mewajibkan verifikasi signature pada sisi penerima. Tujuannya sederhana: memastikan request benar-benar berasal dari pengirim yang sah, bukan dari attacker yang menemukan URL endpoint kita.
MERN Stack Development
Launch your journey into full-stack web development with this comprehensive, pro...
Mekanisme yang umum digunakan adalah HMAC SHA-256. Pengirim dan penerima berbagi sebuah secret key yang tidak pernah dikirim melalui jaringan. Pengirim menghitung hash dari body request menggunakan secret tersebut, lalu menaruh hasilnya pada header signature. Penerima menghitung ulang hash dari body yang diterima, kemudian membandingkan hasilnya dengan nilai di header. Jika keduanya sama, request dianggap sah.
Format header bervariasi antar penyedia. GitHub menggunakan X-Hub-Signature-256 dengan awalan sha256=, sedangkan Stripe menggunakan Stripe-Signature dengan skema t=timestamp,v1=signature. Kode verifikasi kita perlu menyesuaikan format ini, tetapi konsep intinya tetap sama.
Perbandingan signature tidak boleh menggunakan operator === biasa. Perbedaan panjang waktu eksekusi antara dua string bisa bocor lewat timing attack. Node.js menyediakan crypto.timingSafeEqual untuk membandingkan dua buffer dalam waktu yang konstan, berapapun jumlah byte yang sama.
Gambar: Alur verifikasi signature HMAC SHA-256 dari pengirim hingga keputusan terima atau tolak — Sumber: Ilustrasi original untuk artikel ini
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = signatureHeader.replace('sha256=', '');
const expectedBuffer = Buffer.from(expected, 'hex');
const receivedBuffer = Buffer.from(received, 'hex');
if (expectedBuffer.length !== receivedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
}Output:
Signature diterima di header: sha256=addb253d466215cbcca051d71bffd3b132d7049ebc006d186978c8ca7497cc3a
Verifikasi payload asli : true
Verifikasi payload tamper : falseFungsi ini menghitung HMAC dari rawBody menggunakan secret yang sama dengan pengirim. Header yang masuk dibersihkan dari awalan sha256=, lalu kedua nilai dikonversi menjadi Buffer agar bisa dibandingkan dengan timingSafeEqual. Ketika panjang buffer berbeda, fungsi langsung mengembalikan false, karena timingSafeEqual akan melempar error jika kedua buffer memiliki panjang berbeda.
Di dalam handler, kita memanggil fungsi ini sebelum processing dimulai. Jika verifikasi gagal, endpoint mengembalikan status 401 dan mengabaikan payload. Request yang lolos verifikasi baru diteruskan ke langkah berikutnya.
Pemrosesan Event Asinkron dengan Antrian dan Idempotency
Memproses payload berat langsung di dalam handler adalah kesalahan yang sering terjadi. Operasi seperti mengirim email, memanggil API lain, atau menulis ke database bisa memakan waktu ratusan milidetik. Selama waktu tersebut, event loop Node.js terblokir, sehingga request lain ikut tertunda. Handler harus selesai secepat mungkin, dan pekerjaan berat dipindahkan ke luar jalur utama.
Pattern paling sederhana adalah antrian in-memory. Handler cukup menambahkan event ke dalam antrian, lalu sebuah worker mengambil event satu per satu dan memprosesnya di luar request cycle. Dengan cara ini, response 200 tetap cepat dan pekerjaan berat berjalan secara berurutan tanpa memblokir server.
Ada satu tantangan yang tidak bisa dihindari: pengirim webhook melakukan retry ketika tidak menerima response. Retry berarti event yang sama bisa tiba lebih dari sekali. Jika kita memprosesnya berulang kali, data bisa menjadi duplikat. Di sinilah konsep idempotency berperan. Kita menyimpan event ID yang sudah diproses, dan sebelum memproses sebuah event, kita memeriksa apakah event tersebut pernah dikerjakan.
const processedEvents = new Set();
const queue = [];
let processing = false;
function enqueue(event) {
queue.push(event);
processQueue();
}
async function processQueue() {
if (processing) {
return;
}
processing = true;
while (queue.length > 0) {
const event = queue.shift();
if (processedEvents.has(event.eventId)) {
console.log(`[queue] Event ${event.eventId} sudah diproses, dilewati`);
continue;
}
await processEvent(event);
processedEvents.add(event.eventId);
}
processing = false;
}
async function processEvent(event) {
console.log(`[queue] Memproses event ${event.eventId}`);
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(`[queue] Event ${event.eventId} selesai`);
}Output:
[queue] Memproses event evt_001
[queue] Event evt_001 selesai
[queue] Memproses event evt_002
[queue] Event evt_002 selesai
[queue] Event evt_001 sudah diproses, dilewatiHandler memanggil enqueue dengan payload yang sudah diverifikasi. processQueue memastikan hanya satu worker yang berjalan pada satu waktu lewat flag processing. Setiap event yang keluar dari antrian dicek terhadap processedEvents. Jika event ID sudah ada, event dilewati dan tidak diproses ulang. Jika belum, worker menjalankan processEvent lalu mencatat event ID sebagai sudah diproses.
Set berbasis memori hanya cocok untuk satu instance proses. Di produksi dengan banyak instance atau proses restart, data di Set akan hilang. Solusi yang lebih kuat adalah BullMQ yang menggunakan Redis sebagai backend, sehingga antrian persisten dan idempotency bisa disimpan dalam penyimpanan bersama.
Retry, Monitoring, dan Checklist Produksi
Sisi pengirim webhook juga punya tanggung jawab terhadap keandalan pengiriman. Sebagian besar layanan besar menerapkan exponential backoff, yaitu retry dengan jeda yang semakin panjang setelah setiap kegagalan. Jeda awal bisa beberapa detik, lalu meningkat menjadi menit hingga jam, sampai batas maksimum percobaan. Sebagai penerima, kita perlu memastikan endpoint selalu siap, karena retry berarti event bisa datang dalam waktu yang tidak menentu.
Logging menjadi fondasi observability untuk webhook. Setiap event yang masuk dicatat dengan event ID, waktu terima, durasi proses, dan status akhir. Log yang konsisten memudahkan kita menelusuri sebuah event dari awal hingga selesai. Ketika sebuah integrasi bermasalah, tim cukup mencari event ID di log untuk menemukan titik kegagalannya.
Endpoint webhook adalah jalur yang terbuka ke internet, jadi endpoint ini juga perlu dilindungi. Rate limiting membatasi jumlah request dari satu sumber dalam periode tertentu, sehingga attacker tidak bisa membanjiri server dengan payload palsu. Validasi payload memastikan data yang masuk memiliki struktur yang diharapkan sebelum diproses. Keduanya berjalan berdampingan dengan verifikasi signature, bukan menggantikannya.
Checklist untuk webhook production-ready secara singkat: verifikasi signature wajib, response 2xx dikembalikan cepat, pemrosesan berat dilakukan asinkron, idempotency memakai event ID, logging lengkap untuk observability, dan rate limiting melindungi endpoint. Enam poin ini menutup celah paling umum yang membuat integrasi webhook rapuh.
Implementasi webhook menuntut pemahaman tentang HTTP, keamanan signature, dan arsitektur asinkron. Ketiga hal ini adalah materi inti dalam kurikulum web development dan backend engineering di Rumah Coding. Bergabunglah di kelas kami untuk membangun API production-grade dengan Node.js, lengkap dengan praktik keamanan dan pattern yang benar.
Kursus Terkait
MERN Stack Development
Launch your journey into full-stack web development with this comprehensive, project-driven course. Designed for beginners, this course demystifies the MERN stack (MongoDB, Express.js, React.js, Node.js) by guiding you step-by-step in building a real-world application from scratch. By the end of this course, you will have the practical skills and a complete portfolio project to confidently step into the modern web development industry.
EduStream - Mini Learning Management System (LMS)
- Secure Authentication & Authorization: Robust user registration and login using JWT, with strict role-based access control (Admin/Instructor vs. Student).
- Course Management (Admin Dashboard): Full CRUD (Create, Read, Update, Delete) capabilities for administrators to manage course details, including titles, descriptions, pricing, and thumbnail image uploads.
- Public Course Catalog: An interactive and responsive storefront where users can browse available courses.