Eksplorasi mendalam tentang protokol SOAP, sejarah, konsep inti, dan implementasi praktis dengan NestJS. Pelajari kapan SOAP masih relevan dalam sistem terdistribusi modern.

Di era yang didominasi oleh REST API, GraphQL, dan gRPC, SOAP (Simple Object Access Protocol) mungkin terlihat seperti peninggalan dari awal tahun 2000-an. Namun, SOAP terus mendukung sistem enterprise kritis, transaksi finansial, dan layanan pemerintah di seluruh dunia. Memahami SOAP bukan hanya tentang memelihara sistem legacy—ini tentang mengenali kapan kontrak yang ketat, keamanan bawaan, dan reliabilitas transaksional lebih penting daripada kenyamanan developer.
Deep dive ini mengeksplorasi arsitektur SOAP, membandingkannya dengan alternatif modern, dan mendemonstrasikan implementasi production-grade menggunakan NestJS. Jika Anda pernah bertanya-tanya mengapa bank masih menggunakan SOAP atau kapan Anda harus memilihnya daripada REST, artikel ini memberikan jawabannya.
SOAP muncul pada tahun 1998 ketika Microsoft dan DevelopMentor berkolaborasi untuk menyelesaikan masalah fundamental: bagaimana memungkinkan komunikasi terstruktur antara sistem terdistribusi melalui HTTP. Sebelum SOAP, remote procedure calls (RPC) mengandalkan protokol proprietary seperti DCOM dan CORBA, yang kesulitan dengan firewall dan kurang platform independence.
Inovasi kunci SOAP adalah menggunakan XML melalui HTTP, membuatnya firewall-friendly dan platform-agnostic. W3C menstandarisasi SOAP pada tahun 2003, menetapkannya sebagai fondasi untuk web services bersama dengan WSDL (Web Services Description Language) dan UDDI (Universal Description, Discovery, and Integration).
SOAP memperkenalkan beberapa konsep yang groundbreaking untuk distributed computing:
Memahami kapan menggunakan SOAP memerlukan perbandingan dengan alternatif modern. Setiap protokol menyelesaikan masalah yang berbeda.
REST (Representational State Transfer) mendominasi API web modern, tetapi SOAP menawarkan keunggulan yang berbeda dalam skenario spesifik.
| Aspek | SOAP | REST |
|---|---|---|
| Kontrak | Skema WSDL ketat | OpenAPI/Swagger opsional |
| Format Data | XML saja | JSON, XML, lainnya |
| State | Bisa stateful | Stateless by design |
| Keamanan | WS-Security bawaan | OAuth, JWT (eksternal) |
| Transaksi | WS-AtomicTransaction | Level aplikasi |
| Error Handling | Fault terstandarisasi | HTTP status codes |
| Tooling | Client auto-generated | Manual atau codegen |
| Performa | Lebih berat (XML parsing) | Lebih ringan (JSON) |
| Caching | Terbatas | HTTP caching |
Gunakan SOAP ketika: Anda membutuhkan guaranteed message delivery, transaksi ACID lintas service, atau kontrak formal dengan validasi otomatis.
Gunakan REST ketika: Anda membutuhkan kesederhanaan, caching, operasi stateless, atau API public-facing.
GraphQL merevolusi data fetching dengan membiarkan client menentukan persis apa yang mereka butuhkan. Namun, ia melayani tujuan yang berbeda dari SOAP.
| Aspek | SOAP | GraphQL |
|---|---|---|
| Fleksibilitas Query | Operasi tetap | Query yang didefinisikan client |
| Skema | WSDL (XML) | SDL (GraphQL Schema) |
| Versioning | Versi eksplisit | Evolusi skema |
| Real-time | WS-Notification | Subscriptions |
| Kompleksitas | Learning curve tinggi | Kompleksitas sedang |
| Use Case | Integrasi enterprise | Data fetching frontend |
Gunakan SOAP ketika: Anda mengintegrasikan dengan sistem enterprise yang memerlukan kontrak formal dan tidak memerlukan querying fleksibel.
Gunakan GraphQL ketika: Anda membangun API client-facing di mana client berbeda membutuhkan bentuk data berbeda.
gRPC merepresentasikan RPC modern yang dilakukan dengan benar, menggunakan Protocol Buffers dan HTTP/2. Ini adalah penerus spiritual SOAP untuk skenario high-performance.
| Aspek | SOAP | gRPC |
|---|---|---|
| Encoding | XML (text) | Protocol Buffers (binary) |
| Transport | HTTP/1.1, SMTP, TCP | HTTP/2 saja |
| Streaming | Terbatas | Bidirectional streaming |
| Performa | Lebih lambat (overhead XML) | Lebih cepat (binary, multiplexing) |
| Dukungan Browser | Ya (via HTTP) | Terbatas (butuh proxy) |
| Maturity | 25+ tahun | ~8 tahun |
| Ekosistem | Ekstensif (legacy) | Berkembang pesat |
Gunakan SOAP ketika: Anda perlu mengintegrasikan dengan service SOAP yang ada atau memerlukan standar WS-* (security, transactions).
Gunakan gRPC ketika: Anda membangun microservices yang membutuhkan high performance, streaming, atau strong typing tanpa constraint legacy.
Meskipun usianya, SOAP tetap menjadi pilihan yang tepat dalam domain spesifik:
Bank dan payment processor menggunakan SOAP karena:
Contoh dunia nyata: SWIFT (Society for Worldwide Interbank Financial Telecommunication) menggunakan SOAP untuk transfer uang internasional.
Pertukaran rekam medis mengandalkan SOAP untuk:
Instansi pemerintah menyukai SOAP karena:
Operator telekomunikasi menggunakan SOAP untuk:
Memahami SOAP memerlukan pemahaman building blocks fundamentalnya.
Pesan SOAP terdiri dari envelope XML dengan header opsional dan body wajib:
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:example="http://example.com/payment">
<soap:Header>
<example:Authentication>
<example:Token>abc123xyz</example:Token>
</example:Authentication>
</soap:Header>
<soap:Body>
<example:ProcessPayment>
<example:Amount>100.00</example:Amount>
<example:Currency>USD</example:Currency>
</example:ProcessPayment>
</soap:Body>
</soap:Envelope>Envelope: Elemen root yang mengidentifikasi XML sebagai pesan SOAP. Ini mendefinisikan namespace dan aturan encoding.
Header: Elemen opsional untuk metadata seperti authentication, routing, atau transaction context. Header dapat ditandai sebagai mustUnderstand="true" untuk memaksa pemrosesan.
Body: Berisi data request atau response aktual. Hanya satu elemen body yang diizinkan per pesan.
Fault: Ketika error terjadi, body berisi elemen fault alih-alih response normal.
SOAP menstandarisasi pelaporan error melalui elemen fault:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<soap:Fault>
<soap:Code>
<soap:Value>soap:Sender</soap:Value>
</soap:Code>
<soap:Reason>
<soap:Text xml:lang="id">Jumlah pembayaran tidak valid</soap:Text>
</soap:Reason>
<soap:Detail>
<example:PaymentError>
<example:ErrorCode>INVALID_AMOUNT</example:ErrorCode>
<example:MinAmount>1.00</example:MinAmount>
</example:PaymentError>
</soap:Detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>Kode fault menunjukkan siapa yang bertanggung jawab:
soap:Sender: Error client (seperti HTTP 4xx)soap:Receiver: Error server (seperti HTTP 5xx)soap:MustUnderstand: Header yang diperlukan tidak diprosessoap:VersionMismatch: Ketidakcocokan versi SOAPWSDL adalah bahasa kontrak SOAP. Ini mendefinisikan:
Anggap WSDL sebagai setara dengan OpenAPI/Swagger untuk REST, tetapi dengan typing yang lebih kuat dan pembuatan client otomatis.
Kekuatan nyata SOAP berasal dari ekstensi WS-*:
WS-Security: Enkripsi, signatures, dan authentication di level pesan (bukan hanya transport). Ini berarti pesan tetap aman bahkan saat melewati intermediaries.
WS-ReliableMessaging: Menjamin pengiriman pesan dengan acknowledgments dan retries. Kritis untuk transaksi finansial.
WS-AtomicTransaction: Mengkoordinasikan transaksi ACID lintas multiple services. Jika satu service gagal, semua rollback.
WS-Addressing: Menambahkan informasi routing ke pesan, memungkinkan pola komunikasi asynchronous.
WS-Policy: Mendeklarasikan persyaratan dan kemampuan service, memungkinkan client untuk menegosiasikan fitur.
Standar ini menyelesaikan masalah yang sering developer REST reinvent dengan buruk. Namun, mereka menambahkan kompleksitas signifikan.
Mari kita bangun service SOAP production-grade menggunakan NestJS. Kita akan membuat service payment processing yang mendemonstrasikan pola dunia nyata.
Pertama, install dependencies:
npm i -g @nestjs/cli
nest new soap-payment-service
cd soap-payment-serviceBuat file WSDL yang mendefinisikan kontrak payment service kita:
WSDL ini mendefinisikan payment service dengan operasi pemrosesan pembayaran. Perhatikan gaya document/literal untuk interoperabilitas maksimum.
Buat business logic untuk payment processing:
import { Injectable, Logger } from '@nestjs/common';
import { randomUUID } from 'crypto';
export enum PaymentMethod {
CREDIT_CARD = 'CREDIT_CARD',
DEBIT_CARD = 'DEBIT_CARD',
BANK_TRANSFER = 'BANK_TRANSFER',
}
export interface ProcessPaymentRequest {
transactionId: string;
amount: number;
currency: string;
customerId: string;
paymentMethod: PaymentMethod;
}
export interface ProcessPaymentResponse {
success: boolean;
transactionId: string;
authorizationCode: string;
timestamp: Date;
}
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
private readonly processedPayments = new Map<string, ProcessPaymentResponse>();
async processPayment(request: ProcessPaymentRequest): Promise<ProcessPaymentResponse> {
this.logger.log(`Memproses pembayaran: ${JSON.stringify(request)}`);
if (request.amount <= 0) {
throw new Error('Jumlah harus lebih besar dari nol');
}
const validCurrencies = ['USD', 'EUR', 'IDR'];
if (!validCurrencies.includes(request.currency)) {
throw new Error(`Mata uang tidak valid: ${request.currency}`);
}
await this.simulateGatewayDelay();
const authorizationCode = this.generateAuthCode();
const response: ProcessPaymentResponse = {
success: true,
transactionId: request.transactionId,
authorizationCode,
timestamp: new Date(),
};
this.processedPayments.set(request.transactionId, response);
this.logger.log(`Pembayaran berhasil diproses: ${authorizationCode}`);
return response;
}
private generateAuthCode(): string {
return `AUTH-${Date.now()}-${Math.random().toString(36).substring(7).toUpperCase()}`;
}
private async simulateGatewayDelay(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 100));
}
}Service ini mengimplementasikan payment processing realistis dengan validasi, error handling, dan simulasi integrasi gateway.
Bangun controller yang mengekspos endpoint SOAP:
import { Controller, Post, Req, Res, Get, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
import * as soap from 'soap';
import { PaymentService } from './payment.service';
import { readFileSync } from 'fs';
import { join } from 'path';
@Controller('soap/payment')
export class PaymentSoapController {
private readonly logger = new Logger(PaymentSoapController.name);
private soapServer: soap.Server;
constructor(private readonly paymentService: PaymentService) {}
@Get()
getWsdl(@Res() res: Response) {
const wsdlPath = join(__dirname, '../../wsdl/payment-service.wsdl');
const wsdl = readFileSync(wsdlPath, 'utf8');
res.type('application/xml');
res.send(wsdl);
}
@Post()
async handleSoapRequest(@Req() req: Request, @Res() res: Response) {
if (!this.soapServer) {
await this.initializeSoapServer();
}
this.soapServer.emit('request', req, res);
}
private async initializeSoapServer() {
const wsdlPath = join(__dirname, '../../wsdl/payment-service.wsdl');
const wsdl = readFileSync(wsdlPath, 'utf8');
const serviceImplementation = {
PaymentService: {
PaymentServicePort: {
ProcessPayment: async (args: any) => {
try {
this.logger.log('SOAP ProcessPayment dipanggil');
const request = args.parameters;
const response = await this.paymentService.processPayment({
transactionId: request.transactionId,
amount: parseFloat(request.amount),
currency: request.currency,
customerId: request.customerId,
paymentMethod: request.paymentMethod,
});
return {
parameters: {
success: response.success,
transactionId: response.transactionId,
authorizationCode: response.authorizationCode,
timestamp: response.timestamp.toISOString(),
},
};
} catch (error) {
this.logger.error(`ProcessPayment error: ${error.message}`);
throw {
Fault: {
Code: { Value: 'soap:Sender' },
Reason: { Text: error.message },
Detail: { ErrorCode: 'PAYMENT_PROCESSING_ERROR' },
},
};
}
},
},
},
};
this.soapServer = soap.listen(null, '/soap/payment', serviceImplementation, wsdl);
this.logger.log('SOAP server diinisialisasi');
}
}Controller menangani pengambilan WSDL dan pemrosesan request SOAP. Perhatikan bagaimana error dikonversi ke SOAP fault yang tepat.
Buat test client untuk memverifikasi implementasi:
import * as soap from 'soap';
async function testPaymentService() {
const url = 'http://localhost:3000/soap/payment';
try {
const client = await soap.createClientAsync(url);
console.log('Metode tersedia:', Object.keys(client));
console.log('\n--- Testing ProcessPayment ---');
const paymentRequest = {
parameters: {
transactionId: 'TXN-' + Date.now(),
amount: 99.99,
currency: 'IDR',
customerId: 'CUST-12345',
paymentMethod: 'CREDIT_CARD',
},
};
const paymentResult = await client.ProcessPaymentAsync(paymentRequest);
console.log('Hasil Pembayaran:', JSON.stringify(paymentResult, null, 2));
} catch (error) {
console.error('Test gagal:', error);
}
}
testPaymentService();Jalankan tests:
npm run start:devBanyak tutorial legacy masih merekomendasikan RPC/encoded, tetapi ini menyebabkan masalah interoperabilitas.
Salah:
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<m:GetPrice xmlns:m="http://example.com">
<m:Item xsi:type="xsd:string">Widget</m:Item>
</m:GetPrice>
</soap:Body>Benar:
<soap:Body>
<GetPriceRequest xmlns="http://example.com">
<Item>Widget</Item>
</GetPriceRequest>
</soap:Body>Selalu gunakan gaya document/literal dengan validasi XML Schema yang tepat.
Mengembalikan HTTP 500 dengan error plain text merusak SOAP clients.
Salah:
throw new Error('Pembayaran gagal');Benar:
throw {
Fault: {
Code: { Value: 'soap:Sender' },
Reason: { Text: 'Pembayaran gagal' },
Detail: { ErrorCode: 'INSUFFICIENT_FUNDS' }
}
};Menerima struktur XML apa pun mengalahkan type safety SOAP.
Solusi: Gunakan validasi XML Schema untuk memvalidasi request yang masuk.
Sertakan nomor versi dalam namespace dan nama service:
<definitions
targetNamespace="http://example.com/payment/v2"
name="PaymentServiceV2">Ini memungkinkan menjalankan multiple versi secara bersamaan selama migrasi.
Log setiap request dan response SOAP untuk audit trails.
Ketika memanggil service SOAP lain, gunakan kembali koneksi untuk efisiensi.
Operasi finansial harus idempoten untuk menangani retries dengan aman.
Operasi SOAP bisa lambat. Konfigurasikan timeout di multiple level.
Track metrik spesifik SOAP seperti total requests, success rate, dan average response time.
Meskipun kekuatannya, SOAP adalah pilihan yang salah dalam banyak skenario:
Jika Anda membangun API publik untuk web atau mobile apps, REST atau GraphQL adalah pilihan yang lebih baik. Kompleksitas SOAP menciptakan friction untuk developer eksternal.
Protokol berbasis WebSocket atau Server-Sent Events lebih baik untuk update real-time.
Untuk microservices internal, gRPC menawarkan performa lebih baik dengan type safety serupa.
Jika Anda hanya membaca dan menulis record database, REST dengan JSON lebih sederhana dan lebih maintainable.
Cold start times di AWS Lambda atau platform serupa membuat XML parsing SOAP yang berat menjadi problematik.
SOAP tidak mati—ia hanya terspesialisasi. Meskipun REST, GraphQL, dan gRPC mendominasi pengembangan modern, SOAP tetap esensial untuk integrasi enterprise, layanan finansial, dan sistem yang memerlukan kontrak formal dengan keamanan dan transaksi bawaan.
Insight kunci: SOAP unggul ketika Anda membutuhkan guaranteed message delivery, transaksi ACID, kontrak formal, atau integrasi dengan sistem enterprise yang ada. SOAP kesulitan dengan developer experience, performa, dan fleksibilitas. Untuk API publik, aplikasi real-time, atau operasi CRUD sederhana, alternatif modern adalah pilihan yang lebih baik.
Implementasi NestJS mendemonstrasikan bahwa SOAP dapat hidup berdampingan dengan framework modern. Dengan memahami kekuatan dan keterbatasan SOAP, Anda dapat membuat keputusan arsitektural yang informed daripada menolaknya sebagai teknologi legacy.
SOAP mendukung dua gaya encoding yang mempengaruhi bagaimana data diserialisasi:
Pendekatan modern yang direkomendasikan. Body SOAP berisi dokumen XML yang memvalidasi terhadap XML Schema:
<soap:Body>
<ns:CreateOrder xmlns:ns="http://example.com/orders">
<ns:Order>
<ns:CustomerId>12345</ns:CustomerId>
<ns:Items>
<ns:Item>
<ns:ProductId>ABC</ns:ProductId>
<ns:Quantity>2</ns:Quantity>
</ns:Item>
</ns:Items>
</ns:Order>
</ns:CreateOrder>
</soap:Body>Keuntungan: Bersih, memvalidasi terhadap schema, interoperable, lebih mudah untuk versioning.
Gaya lama yang mengenkode method calls dengan informasi tipe:
<soap:Body>
<ns:CreateOrder xmlns:ns="http://example.com/orders">
<customerId xsi:type="xsd:int">12345</customerId>
<items xsi:type="ns:ArrayOfItem">
<item xsi:type="ns:Item">
<productId xsi:type="xsd:string">ABC</productId>
<quantity xsi:type="xsd:int">2</quantity>
</item>
</items>
</ns:CreateOrder>
</soap:Body>Kerugian: Verbose, lebih sulit untuk memvalidasi, interoperabilitas buruk. Hindari di project baru.
Definisikan tipe TypeScript yang sesuai dengan schema WSDL kita:
export enum PaymentMethod {
CREDIT_CARD = 'CREDIT_CARD',
DEBIT_CARD = 'DEBIT_CARD',
BANK_TRANSFER = 'BANK_TRANSFER',
}
export interface ProcessPaymentRequest {
transactionId: string;
amount: number;
currency: string;
customerId: string;
paymentMethod: PaymentMethod;
}
export interface ProcessPaymentResponse {
success: boolean;
transactionId: string;
authorizationCode: string;
timestamp: Date;
}
export interface RefundPaymentRequest {
originalTransactionId: string;
amount: number;
reason: string;
}
export interface RefundPaymentResponse {
success: boolean;
refundId: string;
timestamp: Date;
}Buat business logic untuk payment processing dengan validasi lengkap:
import { Injectable, Logger } from '@nestjs/common';
import {
ProcessPaymentRequest,
ProcessPaymentResponse,
RefundPaymentRequest,
RefundPaymentResponse,
} from './dto/payment.dto';
import { randomUUID } from 'crypto';
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
// Simulasi integrasi payment gateway
private readonly processedPayments = new Map<string, ProcessPaymentResponse>();
async processPayment(
request: ProcessPaymentRequest,
): Promise<ProcessPaymentResponse> {
this.logger.log(`Memproses pembayaran: ${JSON.stringify(request)}`);
// Validasi amount
if (request.amount <= 0) {
throw new Error('Jumlah harus lebih besar dari nol');
}
// Validasi currency
const validCurrencies = ['USD', 'EUR', 'IDR', 'GBP'];
if (!validCurrencies.includes(request.currency)) {
throw new Error(`Mata uang tidak valid: ${request.currency}`);
}
// Simulasi panggilan payment gateway
await this.simulateGatewayDelay();
// Generate authorization code
const authorizationCode = this.generateAuthCode();
const response: ProcessPaymentResponse = {
success: true,
transactionId: request.transactionId,
authorizationCode,
timestamp: new Date(),
};
// Simpan untuk refund lookup
this.processedPayments.set(request.transactionId, response);
this.logger.log(`Pembayaran berhasil diproses: ${authorizationCode}`);
return response;
}
async refundPayment(
request: RefundPaymentRequest,
): Promise<RefundPaymentResponse> {
this.logger.log(`Memproses refund: ${JSON.stringify(request)}`);
// Verifikasi transaksi original ada
const originalPayment = this.processedPayments.get(
request.originalTransactionId,
);
if (!originalPayment) {
throw new Error(
`Transaksi original tidak ditemukan: ${request.originalTransactionId}`,
);
}
// Validasi refund amount
if (request.amount <= 0) {
throw new Error('Jumlah refund harus lebih besar dari nol');
}
// Simulasi pemrosesan refund
await this.simulateGatewayDelay();
const response: RefundPaymentResponse = {
success: true,
refundId: randomUUID(),
timestamp: new Date(),
};
this.logger.log(`Refund berhasil diproses: ${response.refundId}`);
return response;
}
private generateAuthCode(): string {
return `AUTH-${Date.now()}-${Math.random().toString(36).substring(7).toUpperCase()}`;
}
private async simulateGatewayDelay(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 100));
}
}Service ini mengimplementasikan payment processing realistis dengan validasi, error handling, dan simulasi integrasi gateway.
Hubungkan semuanya dalam NestJS module:
import { Module } from '@nestjs/common';
import { PaymentService } from './payment.service';
import { PaymentSoapController } from './payment-soap.controller';
@Module({
controllers: [PaymentSoapController],
providers: [PaymentService],
exports: [PaymentService],
})
export class PaymentModule {}import { Module } from '@nestjs/common';
import { PaymentModule } from './payment/payment.module';
@Module({
imports: [PaymentModule],
})
export class AppModule {}SOAP memerlukan XML parsing. Konfigurasikan Express middleware:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as xmlparser from 'express-xml-bodyparser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Aktifkan XML body parsing untuk SOAP
app.use(xmlparser());
await app.listen(3000);
console.log('SOAP Payment Service berjalan di http://localhost:3000');
console.log('WSDL tersedia di http://localhost:3000/soap/payment');
}
bootstrap();Buat test client untuk memverifikasi implementasi:
import * as soap from 'soap';
async function testPaymentService() {
const url = 'http://localhost:3000/soap/payment';
try {
// Buat SOAP client
const client = await soap.createClientAsync(url);
console.log('Metode tersedia:', Object.keys(client));
// Test ProcessPayment
console.log('\n--- Testing ProcessPayment ---');
const paymentRequest = {
parameters: {
transactionId: 'TXN-' + Date.now(),
amount: 99.99,
currency: 'IDR',
customerId: 'CUST-12345',
paymentMethod: 'CREDIT_CARD',
},
};
const paymentResult = await client.ProcessPaymentAsync(paymentRequest);
console.log('Hasil Pembayaran:', JSON.stringify(paymentResult, null, 2));
// Test RefundPayment
console.log('\n--- Testing RefundPayment ---');
const refundRequest = {
parameters: {
originalTransactionId: paymentRequest.parameters.transactionId,
amount: 99.99,
reason: 'Customer meminta refund',
},
};
const refundResult = await client.RefundPaymentAsync(refundRequest);
console.log('Hasil Refund:', JSON.stringify(refundResult, null, 2));
// Test error handling
console.log('\n--- Testing Error Handling ---');
try {
await client.ProcessPaymentAsync({
parameters: {
transactionId: 'TXN-ERROR',
amount: -10, // Amount tidak valid
currency: 'IDR',
customerId: 'CUST-12345',
paymentMethod: 'CREDIT_CARD',
},
});
} catch (error) {
console.log('Error yang diharapkan:', error.message);
}
} catch (error) {
console.error('Test gagal:', error);
}
}
testPaymentService();Jalankan tests:
npm run start:devOutput yang diharapkan:
Metode tersedia: [ 'ProcessPayment', 'RefundPayment' ]
--- Testing ProcessPayment ---
Hasil Pembayaran: {
"parameters": {
"success": true,
"transactionId": "TXN-1709395200000",
"authorizationCode": "AUTH-1709395200123-X7K9P",
"timestamp": "2026-03-02T10:00:00.000Z"
}
}
--- Testing RefundPayment ---
Hasil Refund: {
"parameters": {
"success": true,
"refundId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-03-02T10:00:01.000Z"
}
}
--- Testing Error Handling ---
Error yang diharapkan: Jumlah harus lebih besar dari nolUntuk sistem production, implementasikan WS-Security untuk authentication dan encryption:
import * as crypto from 'crypto';
export class WsSecurityHandler {
private readonly validTokens = new Set<string>();
constructor(private readonly secretKey: string) {}
generateToken(username: string): string {
const timestamp = Date.now();
const nonce = crypto.randomBytes(16).toString('base64');
const digest = this.createDigest(username, timestamp, nonce);
const token = Buffer.from(
JSON.stringify({ username, timestamp, nonce, digest })
).toString('base64');
this.validTokens.add(token);
return token;
}
validateToken(token: string): boolean {
try {
const decoded = JSON.parse(
Buffer.from(token, 'base64').toString('utf8')
);
// Cek umur token (maksimal 5 menit)
const age = Date.now() - decoded.timestamp;
if (age > 5 * 60 * 1000) {
return false;
}
// Verifikasi digest
const expectedDigest = this.createDigest(
decoded.username,
decoded.timestamp,
decoded.nonce
);
return decoded.digest === expectedDigest;
} catch {
return false;
}
}
private createDigest(username: string, timestamp: number, nonce: string): string {
const data = `${username}:${timestamp}:${nonce}:${this.secretKey}`;
return crypto.createHash('sha256').update(data).digest('base64');
}
}Integrasikan security ke dalam controller:
import { Controller, Post, Req, Res, Get, Logger, UnauthorizedException } from '@nestjs/common';
import { Request, Response } from 'express';
import * as soap from 'soap';
import { WsSecurityHandler } from './security/ws-security';
@Controller('soap/payment')
export class PaymentSoapController {
private readonly logger = new Logger(PaymentSoapController.name);
private soapServer: soap.Server;
private readonly security: WsSecurityHandler;
constructor(private readonly paymentService: PaymentService) {
this.security = new WsSecurityHandler(process.env.WS_SECURITY_KEY || 'default-secret');
}
@Post()
async handleSoapRequest(@Req() req: Request, @Res() res: Response) {
// Extract dan validasi WS-Security header
const authHeader = req.headers['x-ws-security-token'];
if (!authHeader || !this.security.validateToken(authHeader as string)) {
this.logger.warn('Request SOAP tidak terotorisasi');
res.status(401).send(this.createSoapFault('Tidak terotorisasi', 'soap:Sender'));
return;
}
if (!this.soapServer) {
await this.initializeSoapServer();
}
this.soapServer.emit('request', req, res);
}
private createSoapFault(message: string, code: string): string {
return `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<soap:Fault>
<soap:Code><soap:Value>${code}</soap:Value></soap:Code>
<soap:Reason><soap:Text>${message}</soap:Text></soap:Reason>
</soap:Fault>
</soap:Body>
</soap:Envelope>`;
}
}Jangan mencoba membuat SOAP menjadi RESTful atau sebaliknya.
Salah: Menggunakan HTTP status codes untuk business logic di SOAP
if (paymentFailed) {
res.status(402).send(soapResponse); // Jangan lakukan ini
}Benar: Selalu kembalikan HTTP 200 untuk pemrosesan SOAP yang berhasil, gunakan SOAP faults untuk errors
res.status(200).send(soapFaultResponse);XML namespaces sangat kritis di SOAP. Namespace yang tidak cocok menyebabkan kegagalan parsing.
Salah:
<ProcessPayment>
<Amount>100</Amount>
</ProcessPayment>Benar:
<tns:ProcessPayment xmlns:tns="http://example.com/payment">
<tns:Amount>100</tns:Amount>
</tns:ProcessPayment>Mari kita perluas payment service kita untuk berintegrasi dengan API SOAP bank fiktif, mendemonstrasikan skenario enterprise realistis.
Payment service Anda perlu memverifikasi saldo akun sebelum memproses pembayaran dengan memanggil service SOAP bank. Bank memerlukan authentication WS-Security dan menyediakan kontrak WSDL.
import { Injectable, Logger } from '@nestjs/common';
import * as soap from 'soap';
export interface AccountBalanceRequest {
accountNumber: string;
customerId: string;
}
export interface AccountBalanceResponse {
accountNumber: string;
availableBalance: number;
currency: string;
lastUpdated: Date;
}
@Injectable()
export class BankSoapClient {
private readonly logger = new Logger(BankSoapClient.name);
private client: soap.Client;
private readonly wsdlUrl = process.env.BANK_WSDL_URL || 'http://bank-api.example.com/soap?wsdl';
async initialize() {
if (!this.client) {
this.client = await soap.createClientAsync(this.wsdlUrl, {
timeout: 30000,
connectionTimeout: 5000,
});
// Tambahkan WS-Security
const wsSecurity = new soap.WSSecurity(
process.env.BANK_USERNAME,
process.env.BANK_PASSWORD,
{
hasTimeStamp: true,
hasTokenCreated: true,
}
);
this.client.setSecurity(wsSecurity);
this.logger.log('Bank SOAP client diinisialisasi');
}
}
async getAccountBalance(request: AccountBalanceRequest): Promise<AccountBalanceResponse> {
await this.initialize();
try {
const result = await this.client.GetAccountBalanceAsync({
parameters: {
accountNumber: request.accountNumber,
customerId: request.customerId,
},
});
const response = result[0].parameters;
return {
accountNumber: response.accountNumber,
availableBalance: parseFloat(response.availableBalance),
currency: response.currency,
lastUpdated: new Date(response.lastUpdated),
};
} catch (error) {
this.logger.error(`Bank API error: ${error.message}`);
throw new Error(`Gagal mengambil saldo akun: ${error.message}`);
}
}
async verifyFunds(accountNumber: string, customerId: string, requiredAmount: number): Promise<boolean> {
const balance = await this.getAccountBalance({ accountNumber, customerId });
return balance.availableBalance >= requiredAmount;
}
}import { Injectable, Logger } from '@nestjs/common';
import {
ProcessPaymentRequest,
ProcessPaymentResponse,
} from './dto/payment.dto';
import { BankSoapClient } from '../integrations/bank/bank-soap.client';
import { randomUUID } from 'crypto';
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
constructor(private readonly bankClient: BankSoapClient) {}
async processPayment(
request: ProcessPaymentRequest,
): Promise<ProcessPaymentResponse> {
this.logger.log(`Memproses pembayaran: ${JSON.stringify(request)}`);
// Validasi amount
if (request.amount <= 0) {
throw new Error('Jumlah harus lebih besar dari nol');
}
// Untuk bank transfers, verifikasi dana via SOAP call
if (request.paymentMethod === 'BANK_TRANSFER') {
const hasFunds = await this.bankClient.verifyFunds(
request.customerId, // Menggunakan customerId sebagai account number untuk demo
request.customerId,
request.amount,
);
if (!hasFunds) {
throw new Error('Dana tidak mencukupi di akun');
}
this.logger.log('Dana berhasil diverifikasi');
}
// Simulasi pemrosesan pembayaran
await this.simulateGatewayDelay();
const authorizationCode = this.generateAuthCode();
const response: ProcessPaymentResponse = {
success: true,
transactionId: request.transactionId,
authorizationCode,
timestamp: new Date(),
};
this.logger.log(`Pembayaran berhasil diproses: ${authorizationCode}`);
return response;
}
private generateAuthCode(): string {
return `AUTH-${Date.now()}-${Math.random().toString(36).substring(7).toUpperCase()}`;
}
private async simulateGatewayDelay(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 100));
}
}Lindungi service Anda dari kegagalan bank API:
import { Injectable, Logger } from '@nestjs/common';
enum CircuitState {
CLOSED = 'CLOSED',
OPEN = 'OPEN',
HALF_OPEN = 'HALF_OPEN',
}
@Injectable()
export class CircuitBreaker {
private readonly logger = new Logger(CircuitBreaker.name);
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime: number = 0;
private readonly failureThreshold = 5;
private readonly resetTimeout = 60000; // 1 menit
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.logger.log('Circuit breaker memasuki state HALF_OPEN');
this.state = CircuitState.HALF_OPEN;
} else {
throw new Error('Circuit breaker OPEN - service tidak tersedia');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
if (this.state === CircuitState.HALF_OPEN) {
this.logger.log('Circuit breaker menutup setelah panggilan berhasil');
this.state = CircuitState.CLOSED;
}
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.logger.warn(`Circuit breaker membuka setelah ${this.failureCount} kegagalan`);
this.state = CircuitState.OPEN;
}
}
getState(): CircuitState {
return this.state;
}
}Integrasikan circuit breaker:
@Injectable()
export class BankSoapClient {
private readonly logger = new Logger(BankSoapClient.name);
private client: soap.Client;
private readonly circuitBreaker = new CircuitBreaker();
async getAccountBalance(request: AccountBalanceRequest): Promise<AccountBalanceResponse> {
return this.circuitBreaker.execute(async () => {
await this.initialize();
const result = await this.client.GetAccountBalanceAsync({
parameters: request,
});
return this.parseBalanceResponse(result);
});
}
}Buat integration tests dengan mocked SOAP responses:
import { Test, TestingModule } from '@nestjs/testing';
import { BankSoapClient } from './bank-soap.client';
import * as soap from 'soap';
jest.mock('soap');
describe('BankSoapClient', () => {
let client: BankSoapClient;
let mockSoapClient: any;
beforeEach(async () => {
mockSoapClient = {
GetAccountBalanceAsync: jest.fn(),
setSecurity: jest.fn(),
};
(soap.createClientAsync as jest.Mock).mockResolvedValue(mockSoapClient);
const module: TestingModule = await Test.createTestingModule({
providers: [BankSoapClient],
}).compile();
client = module.get<BankSoapClient>(BankSoapClient);
});
describe('getAccountBalance', () => {
it('harus mengembalikan saldo akun dengan sukses', async () => {
const mockResponse = [{
parameters: {
accountNumber: 'ACC-12345',
availableBalance: '1000.00',
currency: 'IDR',
lastUpdated: '2026-03-02T10:00:00Z',
},
}];
mockSoapClient.GetAccountBalanceAsync.mockResolvedValue(mockResponse);
const result = await client.getAccountBalance({
accountNumber: 'ACC-12345',
customerId: 'CUST-12345',
});
expect(result).toEqual({
accountNumber: 'ACC-12345',
availableBalance: 1000.00,
currency: 'IDR',
lastUpdated: expect.any(Date),
});
});
it('harus menangani SOAP faults', async () => {
mockSoapClient.GetAccountBalanceAsync.mockRejectedValue(
new Error('Akun tidak ditemukan')
);
await expect(
client.getAccountBalance({
accountNumber: 'INVALID',
customerId: 'CUST-12345',
})
).rejects.toThrow('Gagal mengambil saldo akun');
});
});
describe('verifyFunds', () => {
it('harus mengembalikan true ketika dana mencukupi', async () => {
mockSoapClient.GetAccountBalanceAsync.mockResolvedValue([{
parameters: {
accountNumber: 'ACC-12345',
availableBalance: '1000.00',
currency: 'IDR',
lastUpdated: '2026-03-02T10:00:00Z',
},
}]);
const result = await client.verifyFunds('ACC-12345', 'CUST-12345', 500);
expect(result).toBe(true);
});
it('harus mengembalikan false ketika dana tidak mencukupi', async () => {
mockSoapClient.GetAccountBalanceAsync.mockResolvedValue([{
parameters: {
accountNumber: 'ACC-12345',
availableBalance: '100.00',
currency: 'IDR',
lastUpdated: '2026-03-02T10:00:00Z',
},
}]);
const result = await client.verifyFunds('ACC-12345', 'CUST-12345', 500);
expect(result).toBe(false);
});
});
});Jalankan tests:
npm run testOverhead XML SOAP memerlukan optimasi yang hati-hati untuk sistem high-throughput.
Kompres pesan SOAP untuk mengurangi bandwidth:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as compression from 'compression';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Aktifkan kompresi GZIP
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
threshold: 1024, // Hanya kompres response > 1KB
}));
await app.listen(3000);
}
bootstrap();GZIP biasanya mengurangi ukuran pesan SOAP sebesar 70-80%.
Untuk transfer data besar, stream XML alih-alih buffering:
import { createReadStream } from 'fs';
import { pipeline } from 'stream/promises';
@Get('large-report')
async getLargeReport(@Res() res: Response) {
res.setHeader('Content-Type', 'application/soap+xml');
const xmlStream = createReadStream('./large-report.xml');
await pipeline(xmlStream, res);
}Parse file WSDL sekali saat startup, bukan per request:
@Injectable()
export class WsdlCacheService {
private readonly cache = new Map<string, any>();
async getWsdl(path: string): Promise<any> {
if (!this.cache.has(path)) {
const wsdl = await this.parseWsdl(path);
this.cache.set(path, wsdl);
}
return this.cache.get(path);
}
private async parseWsdl(path: string): Promise<any> {
// Parse WSDL sekali
return readFileSync(path, 'utf8');
}
}Gabungkan multiple operasi ke dalam satu SOAP call:
<soap:Body>
<BatchRequest xmlns="http://example.com/payment">
<Operations>
<ProcessPayment>
<transactionId>TXN-001</transactionId>
<amount>100.00</amount>
</ProcessPayment>
<ProcessPayment>
<transactionId>TXN-002</transactionId>
<amount>200.00</amount>
</ProcessPayment>
</Operations>
</BatchRequest>
</soap:Body>Ini mengurangi network round trips secara signifikan.
Service SOAP production memerlukan monitoring komprehensif.
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class SoapLogger {
private readonly logger = new Logger('SOAP');
logRequest(operation: string, request: any, correlationId: string) {
this.logger.log({
event: 'soap_request',
operation,
correlationId,
timestamp: new Date().toISOString(),
requestSize: JSON.stringify(request).length,
});
}
logResponse(operation: string, response: any, correlationId: string, duration: number) {
this.logger.log({
event: 'soap_response',
operation,
correlationId,
timestamp: new Date().toISOString(),
duration,
responseSize: JSON.stringify(response).length,
success: true,
});
}
logError(operation: string, error: Error, correlationId: string, duration: number) {
this.logger.error({
event: 'soap_error',
operation,
correlationId,
timestamp: new Date().toISOString(),
duration,
error: error.message,
stack: error.stack,
});
}
}Implementasikan health endpoints untuk monitoring:
import { Controller, Get } from '@nestjs/common';
import { BankSoapClient } from '../integrations/bank/bank-soap.client';
@Controller('health')
export class HealthController {
constructor(private readonly bankClient: BankSoapClient) {}
@Get()
async check() {
const checks = {
status: 'ok',
timestamp: new Date().toISOString(),
services: {
bankApi: await this.checkBankApi(),
},
};
return checks;
}
private async checkBankApi(): Promise<{ status: string; latency?: number }> {
const start = Date.now();
try {
await this.bankClient.initialize();
return {
status: 'healthy',
latency: Date.now() - start,
};
} catch (error) {
return {
status: 'unhealthy',
latency: Date.now() - start,
};
}
}
}Export metrics untuk monitoring systems:
import { Injectable } from '@nestjs/common';
import { Counter, Histogram, register } from 'prom-client';
@Injectable()
export class MetricsService {
private readonly requestCounter = new Counter({
name: 'soap_requests_total',
help: 'Total number of SOAP requests',
labelNames: ['operation', 'status'],
});
private readonly requestDuration = new Histogram({
name: 'soap_request_duration_seconds',
help: 'SOAP request duration in seconds',
labelNames: ['operation'],
buckets: [0.1, 0.5, 1, 2, 5],
});
recordRequest(operation: string, duration: number, success: boolean) {
this.requestCounter.inc({
operation,
status: success ? 'success' : 'error',
});
this.requestDuration.observe({ operation }, duration / 1000);
}
async getMetrics(): Promise<string> {
return register.metrics();
}
}Jika Anda terjebak dengan SOAP tetapi ingin memodernisasi, pertimbangkan pendekatan ini:
Buat facade REST di atas service SOAP:
@Controller('api/payments')
export class PaymentRestController {
constructor(private readonly soapClient: PaymentSoapClient) {}
@Post()
async createPayment(@Body() dto: CreatePaymentDto) {
// Konversi REST request ke SOAP
const soapRequest = {
parameters: {
transactionId: randomUUID(),
amount: dto.amount,
currency: dto.currency,
customerId: dto.customerId,
paymentMethod: dto.paymentMethod,
},
};
const soapResponse = await this.soapClient.processPayment(soapRequest);
// Konversi SOAP response ke REST
return {
id: soapResponse.transactionId,
authCode: soapResponse.authorizationCode,
timestamp: soapResponse.timestamp,
};
}
}Ini memungkinkan client baru menggunakan REST sambil mempertahankan kompatibilitas SOAP.
Secara bertahap ganti operasi SOAP dengan alternatif modern:
Ganti panggilan SOAP synchronous dengan events asynchronous:
@Injectable()
export class PaymentEventService {
constructor(
private readonly eventBus: EventBus,
private readonly soapService: PaymentService,
) {}
async processPaymentAsync(request: ProcessPaymentRequest) {
// Emit event alih-alih SOAP call synchronous
await this.eventBus.publish(
new PaymentRequestedEvent(request)
);
}
@OnEvent('payment.requested')
async handlePaymentRequested(event: PaymentRequestedEvent) {
// Proses via SOAP di background
const result = await this.soapService.processPayment(event.request);
// Emit completion event
await this.eventBus.publish(
new PaymentCompletedEvent(result)
);
}
}Ini memisahkan client dari sifat synchronous SOAP.