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, kami menggunakan IntegrationService khusus per sistem eksternal. Setiap service menangani: autentikasi, pembangunan request, parsing respons, klasifikasi error (dapat dicoba ulang vs. error permanen), dan logging. Kami 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 membangun sistem ERP di Commsult: 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.
Kami 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 kami 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 — kami 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. Kami 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. Kami mengekspos endpoint /health/integrations yang melakukan ping ke setiap layanan eksternal dan mengembalikan status hijau/kuning/merah. Ini di-polling oleh stack monitoring kami setiap 5 menit. Integrasi yang gagal memicu peringatan Slack ke tim engineering. Kami juga mempertahankan tabel integration_events: setiap panggilan keluar dan webhook masuk dicatat.