Tiga pola tersebut adalah panggilan REST API sinkron (ERP memanggil API eksternal dan menunggu respons), webhook asinkron (sistem eksternal memberi tahu ERP ketika suatu event terjadi), dan pertukaran batch berbasis file (file harian, impor CSV, atau SFTP). Sinkron cocok untuk kueri real-time seperti memeriksa status pembayaran, webhook cocok untuk notifikasi event seperti pembayaran yang dikonfirmasi, dan batch cocok untuk data bervolume tinggi yang tidak memerlukan pembaruan real-time seperti rekonsiliasi harian.
Memanggil payment gateway secara sinkron memaksa pengguna menunggu layanan eksternal yang mungkin lambat atau tidak tersedia sementara, membuat UI ERP terasa tidak responsif. Pendekatan yang disarankan adalah mengirimkan pembayaran ke antrian job Bull dan segera mengembalikan respons optimistis 'pembayaran dikirimkan', lalu memproses panggilan gateway di latar belakang. Ini menjaga pengalaman pengguna tetap cepat bahkan ketika layanan eksternal lambat.
Ketika webhook tiba, handler NestJS memvalidasi tanda tangan, segera membuat job Bull (berbasis Redis), dan mengembalikan HTTP 200 — menjaga waktu pemrosesan di bawah 2 detik agar pengirim tidak mencoba ulang. Worker Bull kemudian memproses job dengan retry exponential backoff saat gagal, dan job yang tidak dapat diselesaikan masuk ke dead-letter queue untuk ditinjau secara manual. Membuat handler idempoten memastikan webhook duplikat tidak dapat membuat catatan ganda.
Pola Circuit Breaker (diimplementasikan dengan library opossum) memantau panggilan keluar ke API eksternal dan, jika API tersebut gagal berulang kali, membuka sirkuit dan mengembalikan respons cache atau default alih-alih terus memanggil layanan yang bermasalah. Ini mencegah dependensi eksternal yang terdegradasi menyebar menjadi perlambatan di seluruh ERP. Pola ini juga mengurangi beban yang tidak perlu pada layanan pihak ketiga yang sedang bermasalah.
Post ini membahas DJP e-Faktur untuk pengiriman faktur PPN (yang menggunakan SOAP API berbasis sertifikat, bukan REST), BPJS untuk pelaporan iuran jaminan sosial bulanan, payment gateway SNAP Bank Indonesia (yang memerlukan tanda tangan HMAC-SHA512 dengan format timestamp tertentu), dan LKPP untuk pengadaan pemerintah. Karena dokumentasi resmi untuk integrasi ini masih minim, post ini menyarankan untuk mendokumentasikan detail integrasi secara menyeluruh secara internal.
Sistem ERP modern tidak beroperasi secara terisolasi. Mereka perlu berbicara dengan payment gateway, platform logistik, sistem e-invoicing, portal pemerintah, dan API marketplace. Arsitektur berbasis event dengan webhook dan message queue dapat mengurangi latensi integrasi dari menit menjadi detik. Di Indonesia, ini sangat relevan: sistem ERP perlu terhubung ke e-Faktur DJP untuk faktur PPN, BPJS untuk pelaporan jaminan sosial, dan payment gateway lokal seperti Midtrans atau Xendit.
Setiap integrasi ERP-ke-eksternal masuk ke salah satu dari tiga pola: panggilan REST API sinkron (ERP memanggil API eksternal dan menunggu respons), webhook asinkron (sistem eksternal memberi tahu ERP ketika sesuatu terjadi), dan pertukaran batch berbasis file (file harian, impor CSV, SFTP). Setiap pola memiliki kasus penggunaan yang sesuai.
Untuk panggilan API keluar dari NestJS, saya menggunakan IntegrationService khusus per sistem eksternal. Setiap service menangani: autentikasi, pembangunan request, parsing respons, klasifikasi error (dapat dicoba ulang vs. error permanen), dan logging. Saya membungkus semua panggilan eksternal dalam pola Circuit Breaker (menggunakan library opossum).
ERP Third-Party Integration Architecture
┌──────────────────────────────────────────────────────┐
│ ERP (NestJS) │
│ │
│ Business Logic → IntegrationModule │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ MidtransAdapter EFakturAdapter BpjsAdapter │
│ (payment gw) (DJP SOAP) (REST API) │
└──────────┬───────────────┬───────────┬───────────────┘
│ │ │
OUTBOUND ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ Midtrans │ │ DJP e-Faktur│ │ BPJS │
│ Payment GW │ │ SOAP API │ │ REST API│
└──────────────┘ └──────────────┘ └──────────┘
INBOUND WEBHOOKS:
External System → POST /webhooks/{provider}
│
▼ (< 200ms — quick return)
┌────────────────────────────────────┐
│ Validate signature │
│ Create Bull job │
│ Return HTTP 200 immediately │
└──────────────────┬─────────────────┘
│
▼
┌────────────────────────────────────┐
│ Bull Worker (async) │
│ Process event │
│ Update ERP state │
│ Retry on failure (exponential) │
│ → Dead Letter Queue after max retries │
└────────────────────────────────────┘Dari pengalaman saya sebagai konsultan ERP lepas: jangan pernah memanggil API eksternal secara sinkron dari request pengguna jika respons tidak kritis atau dapat dicoba ulang. Misalnya, saat memposting pembayaran, jangan buat pengguna menunggu respons payment gateway — kirimkan pembayaran ke antrian job Bull, kembalikan respons optimistis 'pembayaran dikirimkan' ke pengguna, dan proses panggilan gateway secara asinkron.
Webhook dari sistem eksternal tiba di endpoint NestJS sebagai request HTTP POST. Persyaratan implementasi kritis: validasi tanda tangan webhook (setiap provider memiliki mekanisme penandatanganan berbeda — Midtrans menggunakan hash MD5, Xendit menggunakan header x-callback-token), buat handler idempoten, dan kembalikan 200 dengan cepat.
Saya menggunakan Bull (antrian job berbasis Redis) untuk semua pemrosesan integrasi asinkron. Ketika webhook tiba, handler memvalidasi tanda tangan, membuat job di Bull, dan mengembalikan 200. Worker Bull memproses job: mem-parse event, memperbarui status ERP, memicu tindakan hilir. Jika pemrosesan gagal, Bull mencoba ulang dengan exponential backoff.
// NestJS: Uniform Integration Adapter Interface
export interface IntegrationAdapter {
connect(): Promise<void>;
healthCheck(): Promise<boolean>;
send(payload: unknown): Promise<{ referenceId: string }>;
getStatus(referenceId: string): Promise<{ status: string }>;
}
// Example: Midtrans adapter
@Injectable()
export class MidtransAdapter implements IntegrationAdapter {
private breaker: CircuitBreaker;
constructor() {
this.breaker = new CircuitBreaker(this.callMidtrans.bind(this), {
timeout: 5000, // 5s timeout
errorThresholdPercentage: 50,
resetTimeout: 30000, // 30s before retry after open
});
}
async send(payload: CreatePaymentPayload) {
return this.breaker.fire(payload);
}
private async callMidtrans(payload: CreatePaymentPayload) {
const res = await fetch('https://api.midtrans.com/v2/charge', {
method: 'POST',
headers: {
'Authorization': 'Basic ' + Buffer.from(
process.env.MIDTRANS_SERVER_KEY + ':'
).toString('base64'),
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Midtrans error: ${res.status}`);
return res.json();
}
}
// Bull queue: reliable webhook processing
@Processor('webhook-events')
export class WebhookProcessor {
@Process('midtrans-payment')
async handleMidtransPayment(@InjectQueue() job: Job<MidtransEvent>) {
// Idempotent: skip if already processed
const exists = await this.paymentRepo.findByReference(job.data.transaction_id);
if (exists) return;
await this.arService.recordPayment(job.data);
this.logger.log(`Payment processed: ${job.data.transaction_id}`);
}
}Lapisan integrasi saya adalah modul NestJS khusus dengan adapter per sistem eksternal. Antarmuka adapter adalah: connect(), healthCheck(), send(payload), dan getStatus(referenceId). Antarmuka seragam ini memudahkan penggantian provider — saya beralih dari satu SMS gateway ke gateway lain tanpa mengubah logika bisnis apa pun, hanya implementasi adapter.
API eksternal memiliki batas rate. API e-Faktur DJP memiliki batas per jam yang dapat dengan mudah terlampaui selama pembuatan faktur PPN akhir bulan ketika banyak faktur dikirimkan sekaligus. Saya melakukan batch pengiriman e-Faktur dengan rate limiter untuk tetap dalam batas. Secara terpisah: kredensial API kedaluwarsa atau dirotasi. Gunakan layanan manajemen secret atau setidaknya tabel database untuk kredensial dengan mekanisme reload.
Integrasi yang spesifik untuk konteks bisnis Indonesia: DJP e-Faktur (pengiriman faktur PPN melalui SOAP API — ya, ini SOAP, bukan REST), API BPJS (pelaporan iuran bulanan), API payment gateway Bank Indonesia (SNAP — Standard National Open API untuk interoperabilitas perbankan), dan LKPP untuk perusahaan yang menjual ke entitas pemerintah.
Pemantauan kesehatan integrasi sangat penting. Saya mengekspos endpoint /health/integrations yang melakukan ping ke setiap layanan eksternal dan mengembalikan status hijau/kuning/merah. Ini di-polling oleh stack monitoring saya setiap 5 menit. Integrasi yang gagal langsung memicu peringatan Slack sehingga saya bisa segera merespons. Saya juga mempertahankan tabel integration_events: setiap panggilan keluar dan webhook masuk dicatat.