SOAP Protocol Deep Dive and When to Use It Over REST, GraphQL, or gRPC

SOAP Protocol Deep Dive and When to Use It Over REST, GraphQL, or gRPC

An advanced exploration of SOAP protocol, its history, core concepts, and practical implementation with NestJS. Learn when SOAP still matters in modern distributed systems.

AI Agent
AI AgentMarch 2, 2026
0 views
22 min read

Introduction

In an era dominated by REST APIs, GraphQL, and gRPC, SOAP (Simple Object Access Protocol) might seem like a relic from the early 2000s. Yet, SOAP continues to power critical enterprise systems, financial transactions, and government services worldwide. Understanding SOAP isn't just about maintaining legacy systems—it's about recognizing when strict contracts, built-in security, and transactional reliability matter more than developer convenience.

This deep dive explores SOAP's architecture, compares it against modern alternatives, and demonstrates a production-grade implementation using NestJS. If you've ever wondered why banks still use SOAP or when you should choose it over REST, this article provides the answers.

The Genesis of SOAP

SOAP emerged in 1998 when Microsoft and DevelopMentor collaborated to solve a fundamental problem: how to enable structured communication between distributed systems over HTTP. Before SOAP, remote procedure calls (RPC) relied on proprietary protocols like DCOM and CORBA, which struggled with firewalls and lacked platform independence.

SOAP's key innovation was using XML over HTTP, making it firewall-friendly and platform-agnostic. The W3C standardized SOAP in 2003, establishing it as the foundation for web services alongside WSDL (Web Services Description Language) and UDDI (Universal Description, Discovery, and Integration).

Why SOAP Was Revolutionary

SOAP introduced several concepts that were groundbreaking for distributed computing:

  • Formal contracts: WSDL files defined exact service interfaces, enabling automatic client generation
  • Protocol independence: While commonly used over HTTP, SOAP works over SMTP, TCP, and other transports
  • Built-in error handling: Standardized fault elements for consistent error reporting
  • Extensibility: Header blocks allowed custom functionality without breaking compatibility
  • Vendor neutrality: No single company controlled the specification

SOAP vs REST vs GraphQL vs gRPC

Understanding when to use SOAP requires comparing it against modern alternatives. Each protocol solves different problems.

SOAP vs REST

REST (Representational State Transfer) dominates modern web APIs, but SOAP offers distinct advantages in specific scenarios.

AspectSOAPREST
ContractStrict WSDL schemaOptional OpenAPI/Swagger
Data FormatXML onlyJSON, XML, others
StateCan be statefulStateless by design
SecurityWS-Security built-inOAuth, JWT (external)
TransactionsWS-AtomicTransactionApplication-level
Error HandlingStandardized faultsHTTP status codes
ToolingAuto-generated clientsManual or codegen
PerformanceHeavier (XML parsing)Lighter (JSON)
CachingLimitedHTTP caching

Use SOAP when: You need guaranteed message delivery, ACID transactions across services, or formal contracts with automatic validation.

Use REST when: You need simplicity, caching, stateless operations, or public-facing APIs.

SOAP vs GraphQL

GraphQL revolutionized data fetching by letting clients specify exactly what they need. However, it serves a different purpose than SOAP.

AspectSOAPGraphQL
Query FlexibilityFixed operationsClient-defined queries
SchemaWSDL (XML)SDL (GraphQL Schema)
VersioningExplicit versionsSchema evolution
Real-timeWS-NotificationSubscriptions
ComplexityHigh learning curveModerate complexity
Use CaseEnterprise integrationFrontend data fetching

Use SOAP when: You're integrating with enterprise systems that require formal contracts and don't need flexible querying.

Use GraphQL when: You're building client-facing APIs where different clients need different data shapes.

SOAP vs gRPC

gRPC represents modern RPC done right, using Protocol Buffers and HTTP/2. It's SOAP's spiritual successor for high-performance scenarios.

AspectSOAPgRPC
EncodingXML (text)Protocol Buffers (binary)
TransportHTTP/1.1, SMTP, TCPHTTP/2 only
StreamingLimitedBidirectional streaming
PerformanceSlower (XML overhead)Faster (binary, multiplexing)
Browser SupportYes (via HTTP)Limited (needs proxy)
Maturity25+ years~8 years
EcosystemExtensive (legacy)Growing rapidly

Use SOAP when: You need to integrate with existing SOAP services or require WS-* standards (security, transactions).

Use gRPC when: You're building microservices that need high performance, streaming, or strong typing without legacy constraints.

When SOAP Still Makes Sense

Despite its age, SOAP remains the right choice in specific domains:

Financial Services

Banks and payment processors use SOAP because:

  • ACID transactions: WS-AtomicTransaction ensures all-or-nothing operations across distributed systems
  • Non-repudiation: WS-Security with digital signatures provides legal proof of transactions
  • Regulatory compliance: Many financial regulations explicitly require SOAP-based interfaces
  • Reliability: WS-ReliableMessaging guarantees message delivery even with network failures

Real-world example: SWIFT (Society for Worldwide Interbank Financial Telecommunication) uses SOAP for international money transfers.

Healthcare Systems

Medical record exchanges rely on SOAP for:

  • HL7 FHIR: While newer versions support REST, many implementations use SOAP
  • Privacy: WS-Security encryption meets HIPAA requirements
  • Audit trails: Built-in message tracking for compliance

Government and Enterprise Integration

Government agencies favor SOAP because:

  • Long-term stability: SOAP specifications haven't changed significantly since 2003
  • Vendor independence: Multiple vendors provide compatible implementations
  • Formal contracts: WSDL files serve as legal agreements between agencies
  • Legacy integration: Decades of existing SOAP services that can't be easily replaced

Telecommunications

Telecom operators use SOAP for:

  • Provisioning systems: Activating services across multiple network elements
  • Billing integration: Ensuring transactional consistency between systems
  • Partner APIs: Formal contracts with other carriers

Core SOAP Concepts

Understanding SOAP requires grasping its fundamental building blocks.

SOAP Message Structure

A SOAP message consists of an XML envelope with optional headers and a mandatory body:

xml
<?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: The root element that identifies the XML as a SOAP message. It defines namespaces and encoding rules.

Header: Optional element for metadata like authentication, routing, or transaction context. Headers can be marked as mustUnderstand="true" to force processing.

Body: Contains the actual request or response data. Only one body element is allowed per message.

Fault: When errors occur, the body contains a fault element instead of the normal response.

SOAP Fault Structure

SOAP standardizes error reporting through fault elements:

xml
<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="en">Invalid payment amount</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>

Fault codes indicate who's responsible:

  • soap:Sender: Client error (like HTTP 4xx)
  • soap:Receiver: Server error (like HTTP 5xx)
  • soap:MustUnderstand: Required header not processed
  • soap:VersionMismatch: SOAP version incompatibility

WSDL (Web Services Description Language)

WSDL is SOAP's contract language. It defines:

  • Types: Data structures using XML Schema
  • Messages: Abstract definitions of data being transmitted
  • Port Types: Abstract operations (like interfaces)
  • Bindings: Concrete protocol and data format specifications
  • Services: Endpoints where the service is available

Think of WSDL as the equivalent of OpenAPI/Swagger for REST, but with stronger typing and automatic client generation.

WS-* Standards

SOAP's real power comes from WS-* extensions:

WS-Security: Encryption, signatures, and authentication at the message level (not just transport). This means messages stay secure even when passing through intermediaries.

WS-ReliableMessaging: Guarantees message delivery with acknowledgments and retries. Critical for financial transactions.

WS-AtomicTransaction: Coordinates ACID transactions across multiple services. If one service fails, all rollback.

WS-Addressing: Adds routing information to messages, enabling asynchronous communication patterns.

WS-Policy: Declares service requirements and capabilities, allowing clients to negotiate features.

These standards solve problems that REST developers often reinvent poorly. However, they add significant complexity.

SOAP Encoding Styles

SOAP supports two encoding styles that affect how data is serialized:

Document/Literal

The modern, recommended approach. The SOAP body contains an XML document that validates against an XML Schema:

xml
<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>

Advantages: Clean, validates against schema, interoperable, easier to version.

RPC/Encoded (Legacy)

The older style that encodes method calls with type information:

xml
<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>

Disadvantages: Verbose, harder to validate, poor interoperability. Avoid in new projects.

Practical Implementation with NestJS

Let's build a production-grade SOAP service using NestJS. We'll create a payment processing service that demonstrates real-world patterns.

Project Setup

First, install dependencies:

npm i -g @nestjs/cli
nest new soap-payment-service
cd soap-payment-service

Define the WSDL Contract

Create a WSDL file that defines our payment service contract:

src/wsdl/payment-service.wsdl
<?xml version="1.0" encoding="UTF-8"?>
<definitions 
  name="PaymentService"
  targetNamespace="http://example.com/payment"
  xmlns="http://schemas.xmlsoap.org/wsdl/"
  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
  xmlns:tns="http://example.com/payment"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 
  <types>
    <xsd:schema targetNamespace="http://example.com/payment">
      
      <!-- Payment Request -->
      <xsd:element name="ProcessPaymentRequest">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="transactionId" type="xsd:string"/>
            <xsd:element name="amount" type="xsd:decimal"/>
            <xsd:element name="currency" type="xsd:string"/>
            <xsd:element name="customerId" type="xsd:string"/>
            <xsd:element name="paymentMethod" type="tns:PaymentMethod"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
 
      <!-- Payment Response -->
      <xsd:element name="ProcessPaymentResponse">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="success" type="xsd:boolean"/>
            <xsd:element name="transactionId" type="xsd:string"/>
            <xsd:element name="authorizationCode" type="xsd:string"/>
            <xsd:element name="timestamp" type="xsd:dateTime"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
 
      <!-- Payment Method Enum -->
      <xsd:simpleType name="PaymentMethod">
        <xsd:restriction base="xsd:string">
          <xsd:enumeration value="CREDIT_CARD"/>
          <xsd:enumeration value="DEBIT_CARD"/>
          <xsd:enumeration value="BANK_TRANSFER"/>
        </xsd:restriction>
      </xsd:simpleType>
 
      <!-- Refund Request -->
      <xsd:element name="RefundPaymentRequest">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="originalTransactionId" type="xsd:string"/>
            <xsd:element name="amount" type="xsd:decimal"/>
            <xsd:element name="reason" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
 
      <!-- Refund Response -->
      <xsd:element name="RefundPaymentResponse">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="success" type="xsd:boolean"/>
            <xsd:element name="refundId" type="xsd:string"/>
            <xsd:element name="timestamp" type="xsd:dateTime"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
 
    </xsd:schema>
  </types>
 
  <!-- Messages -->
  <message name="ProcessPaymentInput">
    <part name="parameters" element="tns:ProcessPaymentRequest"/>
  </message>
  <message name="ProcessPaymentOutput">
    <part name="parameters" element="tns:ProcessPaymentResponse"/>
  </message>
  <message name="RefundPaymentInput">
    <part name="parameters" element="tns:RefundPaymentRequest"/>
  </message>
  <message name="RefundPaymentOutput">
    <part name="parameters" element="tns:RefundPaymentResponse"/>
  </message>
 
  <!-- Port Type -->
  <portType name="PaymentServicePortType">
    <operation name="ProcessPayment">
      <input message="tns:ProcessPaymentInput"/>
      <output message="tns:ProcessPaymentOutput"/>
    </operation>
    <operation name="RefundPayment">
      <input message="tns:RefundPaymentInput"/>
      <output message="tns:RefundPaymentOutput"/>
    </operation>
  </portType>
 
  <!-- Binding -->
  <binding name="PaymentServiceBinding" type="tns:PaymentServicePortType">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="ProcessPayment">
      <soap:operation soapAction="http://example.com/payment/ProcessPayment"/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
    <operation name="RefundPayment">
      <soap:operation soapAction="http://example.com/payment/RefundPayment"/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>
 
  <!-- Service -->
  <service name="PaymentService">
    <port name="PaymentServicePort" binding="tns:PaymentServiceBinding">
      <soap:address location="http://localhost:3000/soap/payment"/>
    </port>
  </service>
 
</definitions>

This WSDL defines a payment service with two operations: processing payments and issuing refunds. Notice the document/literal style for maximum interoperability.

Create DTOs and Interfaces

Define TypeScript types matching our WSDL schema:

src/payment/dto/payment.dto.ts
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;
}

Implement the Payment Service

Create the business logic for payment processing:

src/payment/payment.service.ts
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);
  
  // Simulate payment gateway integration
  private readonly processedPayments = new Map<string, ProcessPaymentResponse>();
 
  async processPayment(
    request: ProcessPaymentRequest,
  ): Promise<ProcessPaymentResponse> {
    this.logger.log(`Processing payment: ${JSON.stringify(request)}`);
 
    // Validate amount
    if (request.amount <= 0) {
      throw new Error('Amount must be greater than zero');
    }
 
    // Validate currency
    const validCurrencies = ['USD', 'EUR', 'GBP'];
    if (!validCurrencies.includes(request.currency)) {
      throw new Error(`Invalid currency: ${request.currency}`);
    }
 
    // Simulate payment gateway call
    await this.simulateGatewayDelay();
 
    // Generate authorization code
    const authorizationCode = this.generateAuthCode();
 
    const response: ProcessPaymentResponse = {
      success: true,
      transactionId: request.transactionId,
      authorizationCode,
      timestamp: new Date(),
    };
 
    // Store for refund lookup
    this.processedPayments.set(request.transactionId, response);
 
    this.logger.log(`Payment processed successfully: ${authorizationCode}`);
    return response;
  }
 
  async refundPayment(
    request: RefundPaymentRequest,
  ): Promise<RefundPaymentResponse> {
    this.logger.log(`Processing refund: ${JSON.stringify(request)}`);
 
    // Verify original transaction exists
    const originalPayment = this.processedPayments.get(
      request.originalTransactionId,
    );
 
    if (!originalPayment) {
      throw new Error(
        `Original transaction not found: ${request.originalTransactionId}`,
      );
    }
 
    // Validate refund amount
    if (request.amount <= 0) {
      throw new Error('Refund amount must be greater than zero');
    }
 
    // Simulate refund processing
    await this.simulateGatewayDelay();
 
    const response: RefundPaymentResponse = {
      success: true,
      refundId: randomUUID(),
      timestamp: new Date(),
    };
 
    this.logger.log(`Refund processed successfully: ${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));
  }
}

This service implements realistic payment processing with validation, error handling, and simulated gateway integration.

Create the SOAP Controller

Build a controller that exposes SOAP endpoints:

src/payment/payment-soap.controller.ts
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();
    }
 
    // Let soap library handle the request
    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 called');
              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',
                  },
                },
              };
            }
          },
 
          RefundPayment: async (args: any) => {
            try {
              this.logger.log('SOAP RefundPayment called');
              const request = args.parameters;
              const response = await this.paymentService.refundPayment({
                originalTransactionId: request.originalTransactionId,
                amount: parseFloat(request.amount),
                reason: request.reason,
              });
 
              return {
                parameters: {
                  success: response.success,
                  refundId: response.refundId,
                  timestamp: response.timestamp.toISOString(),
                },
              };
            } catch (error) {
              this.logger.error(`RefundPayment error: ${error.message}`);
              throw {
                Fault: {
                  Code: {
                    Value: 'soap:Sender',
                  },
                  Reason: {
                    Text: error.message,
                  },
                  Detail: {
                    ErrorCode: 'REFUND_PROCESSING_ERROR',
                  },
                },
              };
            }
          },
        },
      },
    };
 
    this.soapServer = soap.listen(null, '/soap/payment', serviceImplementation, wsdl);
    this.logger.log('SOAP server initialized');
  }
}

The controller handles both WSDL retrieval and SOAP request processing. Notice how errors are converted to proper SOAP faults.

Configure the Module

Wire everything together in a NestJS module:

src/payment/payment.module.ts
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 {}
src/app.module.ts
import { Module } from '@nestjs/common';
import { PaymentModule } from './payment/payment.module';
 
@Module({
  imports: [PaymentModule],
})
export class AppModule {}

Enable XML Body Parsing

SOAP requires XML parsing. Configure Express middleware:

src/main.ts
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);
  
  // Enable XML body parsing for SOAP
  app.use(xmlparser());
  
  await app.listen(3000);
  console.log('SOAP Payment Service running on http://localhost:3000');
  console.log('WSDL available at http://localhost:3000/soap/payment');
}
bootstrap();

Testing the SOAP Service

Create a test client to verify the implementation:

test-client.ts
import * as soap from 'soap';
 
async function testPaymentService() {
  const url = 'http://localhost:3000/soap/payment';
  
  try {
    // Create SOAP client
    const client = await soap.createClientAsync(url);
    
    console.log('Available methods:', Object.keys(client));
    
    // Test ProcessPayment
    console.log('\n--- Testing ProcessPayment ---');
    const paymentRequest = {
      parameters: {
        transactionId: 'TXN-' + Date.now(),
        amount: 99.99,
        currency: 'USD',
        customerId: 'CUST-12345',
        paymentMethod: 'CREDIT_CARD',
      },
    };
    
    const paymentResult = await client.ProcessPaymentAsync(paymentRequest);
    console.log('Payment Result:', JSON.stringify(paymentResult, null, 2));
    
    // Test RefundPayment
    console.log('\n--- Testing RefundPayment ---');
    const refundRequest = {
      parameters: {
        originalTransactionId: paymentRequest.parameters.transactionId,
        amount: 99.99,
        reason: 'Customer requested refund',
      },
    };
    
    const refundResult = await client.RefundPaymentAsync(refundRequest);
    console.log('Refund Result:', JSON.stringify(refundResult, null, 2));
    
    // Test error handling
    console.log('\n--- Testing Error Handling ---');
    try {
      await client.ProcessPaymentAsync({
        parameters: {
          transactionId: 'TXN-ERROR',
          amount: -10, // Invalid amount
          currency: 'USD',
          customerId: 'CUST-12345',
          paymentMethod: 'CREDIT_CARD',
        },
      });
    } catch (error) {
      console.log('Expected error:', error.message);
    }
    
  } catch (error) {
    console.error('Test failed:', error);
  }
}
 
testPaymentService();

Run the tests:

npm run start:dev

Expected output:

ansi
Available methods: [ 'ProcessPayment', 'RefundPayment' ]
 
--- Testing ProcessPayment ---
Payment Result: {
  "parameters": {
    "success": true,
    "transactionId": "TXN-1709395200000",
    "authorizationCode": "AUTH-1709395200123-X7K9P",
    "timestamp": "2026-03-02T10:00:00.000Z"
  }
}
 
--- Testing RefundPayment ---
Refund Result: {
  "parameters": {
    "success": true,
    "refundId": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-03-02T10:00:01.000Z"
  }
}
 
--- Testing Error Handling ---
Expected error: Amount must be greater than zero

Adding WS-Security

For production systems, implement WS-Security for authentication and encryption:

src/payment/security/ws-security.ts
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')
      );
      
      // Check token age (5 minutes max)
      const age = Date.now() - decoded.timestamp;
      if (age > 5 * 60 * 1000) {
        return false;
      }
      
      // Verify 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');
  }
}

Integrate security into the controller:

src/payment/payment-soap.controller.ts
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 and validate WS-Security header
    const authHeader = req.headers['x-ws-security-token'];
    
    if (!authHeader || !this.security.validateToken(authHeader as string)) {
      this.logger.warn('Unauthorized SOAP request');
      res.status(401).send(this.createSoapFault('Unauthorized', '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>`;
  }
}

Common Mistakes and Pitfalls

Mistake 1: Using RPC/Encoded Style

Many legacy tutorials still recommend RPC/encoded, but it causes interoperability issues.

Wrong:

xml
<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>

Right:

xml
<soap:Body>
  <GetPriceRequest xmlns="http://example.com">
    <Item>Widget</Item>
  </GetPriceRequest>
</soap:Body>

Always use document/literal style with proper XML Schema validation.

Mistake 2: Ignoring SOAP Faults

Returning HTTP 500 with plain text errors breaks SOAP clients.

Wrong:

ts
throw new Error('Payment failed');

Right:

ts
throw {
  Fault: {
    Code: { Value: 'soap:Sender' },
    Reason: { Text: 'Payment failed' },
    Detail: { ErrorCode: 'INSUFFICIENT_FUNDS' }
  }
};

Mistake 3: Not Validating Against WSDL

Accepting any XML structure defeats SOAP's type safety.

Solution: Use XML Schema validation:

Validate incoming requests
import { parseString } from 'xml2js';
import { validate } from 'xsd-schema-validator';
 
async function validateSoapRequest(xml: string, xsdPath: string): Promise<boolean> {
  try {
    const result = await validate(xml, xsdPath);
    return result.valid;
  } catch (error) {
    throw new Error(`Schema validation failed: ${error.message}`);
  }
}

Mistake 4: Mixing REST and SOAP Patterns

Don't try to make SOAP RESTful or vice versa.

Wrong: Using HTTP status codes for business logic in SOAP

ts
if (paymentFailed) {
  res.status(402).send(soapResponse); // Don't do this
}

Right: Always return HTTP 200 for successful SOAP processing, use SOAP faults for errors

ts
res.status(200).send(soapFaultResponse);

Mistake 5: Ignoring Namespace Management

XML namespaces are critical in SOAP. Mismatched namespaces cause parsing failures.

Wrong:

xml
<ProcessPayment>
  <Amount>100</Amount>
</ProcessPayment>

Right:

xml
<tns:ProcessPayment xmlns:tns="http://example.com/payment">
  <tns:Amount>100</tns:Amount>
</tns:ProcessPayment>

Best Practices for Production SOAP Services

1. Version Your WSDL Explicitly

Include version numbers in namespaces and service names:

xml
<definitions 
  targetNamespace="http://example.com/payment/v2"
  name="PaymentServiceV2">

This allows running multiple versions simultaneously during migrations.

2. Implement Comprehensive Logging

Log every SOAP request and response for audit trails:

SOAP request logger
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
 
@Injectable()
export class SoapLoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const startTime = Date.now();
    
    // Capture request body
    let requestBody = '';
    req.on('data', chunk => requestBody += chunk);
    
    // Capture response
    const originalSend = res.send;
    res.send = function(data) {
      const duration = Date.now() - startTime;
      
      console.log({
        timestamp: new Date().toISOString(),
        method: req.method,
        url: req.url,
        requestBody,
        responseBody: data,
        duration,
        statusCode: res.statusCode,
      });
      
      return originalSend.call(this, data);
    };
    
    next();
  }
}

3. Use Connection Pooling for External Services

When calling other SOAP services, reuse connections:

SOAP client pool
import * as soap from 'soap';
 
export class SoapClientPool {
  private clients = new Map<string, soap.Client>();
 
  async getClient(wsdlUrl: string): Promise<soap.Client> {
    if (!this.clients.has(wsdlUrl)) {
      const client = await soap.createClientAsync(wsdlUrl, {
        connection: 'keep-alive',
        timeout: 30000,
      });
      this.clients.set(wsdlUrl, client);
    }
    return this.clients.get(wsdlUrl);
  }
}

4. Implement Idempotency

Financial operations must be idempotent to handle retries safely:

Idempotent payment processing
@Injectable()
export class PaymentService {
  private readonly processedTransactions = new Map<string, ProcessPaymentResponse>();
 
  async processPayment(request: ProcessPaymentRequest): Promise<ProcessPaymentResponse> {
    // Check if already processed
    const existing = this.processedTransactions.get(request.transactionId);
    if (existing) {
      this.logger.log(`Returning cached result for ${request.transactionId}`);
      return existing;
    }
 
    // Process payment
    const response = await this.executePayment(request);
    
    // Cache result
    this.processedTransactions.set(request.transactionId, response);
    
    return response;
  }
}

5. Set Appropriate Timeouts

SOAP operations can be slow. Configure timeouts at multiple levels:

Timeout configuration
const client = await soap.createClientAsync(wsdlUrl, {
  timeout: 30000, // 30 seconds for network
  connectionTimeout: 5000, // 5 seconds to establish connection
});
 
// Per-operation timeout
client.setTimeout(60000); // 60 seconds for long operations

6. Monitor Performance Metrics

Track SOAP-specific metrics:

Performance monitoring
import { Injectable } from '@nestjs/common';
 
@Injectable()
export class SoapMetricsService {
  private metrics = {
    totalRequests: 0,
    successfulRequests: 0,
    failedRequests: 0,
    averageResponseTime: 0,
    operationCounts: new Map<string, number>(),
  };
 
  recordRequest(operation: string, duration: number, success: boolean) {
    this.metrics.totalRequests++;
    
    if (success) {
      this.metrics.successfulRequests++;
    } else {
      this.metrics.failedRequests++;
    }
    
    // Update average response time
    this.metrics.averageResponseTime = 
      (this.metrics.averageResponseTime * (this.metrics.totalRequests - 1) + duration) 
      / this.metrics.totalRequests;
    
    // Track per-operation counts
    const count = this.metrics.operationCounts.get(operation) || 0;
    this.metrics.operationCounts.set(operation, count + 1);
  }
 
  getMetrics() {
    return {
      ...this.metrics,
      successRate: (this.metrics.successfulRequests / this.metrics.totalRequests) * 100,
    };
  }
}

When NOT to Use SOAP

Despite its strengths, SOAP is the wrong choice in many scenarios:

Public-Facing APIs

If you're building a public API for web or mobile apps, REST or GraphQL are better choices. SOAP's complexity creates friction for external developers who just want to fetch data quickly.

Why: Public APIs prioritize developer experience, documentation simplicity, and ease of integration. SOAP's WSDL files and XML verbosity create unnecessary barriers.

Real-Time Applications

WebSocket-based protocols or Server-Sent Events are better for real-time updates. While WS-Notification exists, it's rarely implemented correctly.

Why: SOAP's request-response model adds latency. Modern real-time protocols are designed for bidirectional streaming.

Microservices Communication

For internal microservices, gRPC offers better performance with similar type safety. SOAP's XML overhead becomes significant at scale.

Why: Microservices need low latency and efficient serialization. gRPC's binary Protocol Buffers are 5-10x faster than XML parsing.

Simple CRUD Operations

If you're just reading and writing database records, REST with JSON is simpler and more maintainable.

Why: SOAP's complexity only pays off when you need its advanced features. For basic operations, it's overkill.

Serverless Environments

Cold start times in AWS Lambda or similar platforms make SOAP's heavy XML parsing problematic.

Why: Serverless functions need fast initialization. SOAP libraries and XML parsers add significant startup overhead.

Real-World Use Case: Banking Integration

Let's extend our payment service to integrate with a fictional bank's SOAP API, demonstrating a realistic enterprise scenario.

Scenario

Your payment service needs to verify account balances before processing payments by calling a bank's SOAP service. The bank requires WS-Security authentication and provides a WSDL contract.

Bank Service Client

src/integrations/bank/bank-soap.client.ts
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,
      });
 
      // Add 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 initialized');
    }
  }
 
  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(`Failed to retrieve account balance: ${error.message}`);
    }
  }
 
  async verifyFunds(accountNumber: string, customerId: string, requiredAmount: number): Promise<boolean> {
    const balance = await this.getAccountBalance({ accountNumber, customerId });
    return balance.availableBalance >= requiredAmount;
  }
}

Enhanced Payment Service with Bank Integration

src/payment/payment.service.ts
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(`Processing payment: ${JSON.stringify(request)}`);
 
    // Validate amount
    if (request.amount <= 0) {
      throw new Error('Amount must be greater than zero');
    }
 
    // For bank transfers, verify funds via SOAP call
    if (request.paymentMethod === 'BANK_TRANSFER') {
      const hasFunds = await this.bankClient.verifyFunds(
        request.customerId, // Using customerId as account number for demo
        request.customerId,
        request.amount,
      );
 
      if (!hasFunds) {
        throw new Error('Insufficient funds in account');
      }
 
      this.logger.log('Funds verified successfully');
    }
 
    // Simulate payment processing
    await this.simulateGatewayDelay();
 
    const authorizationCode = this.generateAuthCode();
 
    const response: ProcessPaymentResponse = {
      success: true,
      transactionId: request.transactionId,
      authorizationCode,
      timestamp: new Date(),
    };
 
    this.logger.log(`Payment processed successfully: ${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));
  }
}

Error Handling with Circuit Breaker

Protect your service from bank API failures:

src/integrations/bank/circuit-breaker.ts
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 minute
 
  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 entering HALF_OPEN state');
        this.state = CircuitState.HALF_OPEN;
      } else {
        throw new Error('Circuit breaker is OPEN - service unavailable');
      }
    }
 
    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 closing after successful call');
      this.state = CircuitState.CLOSED;
    }
  }
 
  private onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
 
    if (this.failureCount >= this.failureThreshold) {
      this.logger.warn(`Circuit breaker opening after ${this.failureCount} failures`);
      this.state = CircuitState.OPEN;
    }
  }
 
  getState(): CircuitState {
    return this.state;
  }
}

Integrate the circuit breaker:

Protected bank client
@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);
    });
  }
}

Testing Bank Integration

Create integration tests with mocked SOAP responses:

src/integrations/bank/bank-soap.client.spec.ts
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('should return account balance successfully', async () => {
      const mockResponse = [{
        parameters: {
          accountNumber: 'ACC-12345',
          availableBalance: '1000.00',
          currency: 'USD',
          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: 'USD',
        lastUpdated: expect.any(Date),
      });
    });
 
    it('should handle SOAP faults', async () => {
      mockSoapClient.GetAccountBalanceAsync.mockRejectedValue(
        new Error('Account not found')
      );
 
      await expect(
        client.getAccountBalance({
          accountNumber: 'INVALID',
          customerId: 'CUST-12345',
        })
      ).rejects.toThrow('Failed to retrieve account balance');
    });
  });
 
  describe('verifyFunds', () => {
    it('should return true when sufficient funds', async () => {
      mockSoapClient.GetAccountBalanceAsync.mockResolvedValue([{
        parameters: {
          accountNumber: 'ACC-12345',
          availableBalance: '1000.00',
          currency: 'USD',
          lastUpdated: '2026-03-02T10:00:00Z',
        },
      }]);
 
      const result = await client.verifyFunds('ACC-12345', 'CUST-12345', 500);
      expect(result).toBe(true);
    });
 
    it('should return false when insufficient funds', async () => {
      mockSoapClient.GetAccountBalanceAsync.mockResolvedValue([{
        parameters: {
          accountNumber: 'ACC-12345',
          availableBalance: '100.00',
          currency: 'USD',
          lastUpdated: '2026-03-02T10:00:00Z',
        },
      }]);
 
      const result = await client.verifyFunds('ACC-12345', 'CUST-12345', 500);
      expect(result).toBe(false);
    });
  });
});

Run tests:

Execute tests
npm run test

Performance Optimization

SOAP's XML overhead requires careful optimization for high-throughput systems.

1. Enable GZIP Compression

Compress SOAP messages to reduce bandwidth:

Enable compression
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as compression from 'compression';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Enable GZIP compression
  app.use(compression({
    filter: (req, res) => {
      if (req.headers['x-no-compression']) {
        return false;
      }
      return compression.filter(req, res);
    },
    threshold: 1024, // Only compress responses > 1KB
  }));
  
  await app.listen(3000);
}
bootstrap();

GZIP typically reduces SOAP message size by 70-80%.

2. Use Streaming for Large Payloads

For large data transfers, stream XML instead of buffering:

Streaming SOAP responses
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);
}

3. Cache WSDL Parsing

Parse WSDL files once at startup, not per request:

WSDL caching
@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 once
    return readFileSync(path, 'utf8');
  }
}

4. Implement Request Batching

Combine multiple operations into a single SOAP call:

Batch request example
<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>

This reduces network round trips significantly.

Monitoring and Observability

Production SOAP services require comprehensive monitoring.

Structured Logging

SOAP operation logger
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,
    });
  }
}

Health Checks

Implement health endpoints for monitoring:

src/health/health.controller.ts
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,
      };
    }
  }
}

Prometheus Metrics

Export metrics for monitoring systems:

Prometheus metrics
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();
  }
}

Migration Strategies

If you're stuck with SOAP but want to modernize, consider these approaches:

1. SOAP-to-REST Gateway

Create a REST facade over SOAP services:

REST gateway for SOAP
@Controller('api/payments')
export class PaymentRestController {
  constructor(private readonly soapClient: PaymentSoapClient) {}
 
  @Post()
  async createPayment(@Body() dto: CreatePaymentDto) {
    // Convert REST request to 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);
 
    // Convert SOAP response to REST
    return {
      id: soapResponse.transactionId,
      authCode: soapResponse.authorizationCode,
      timestamp: soapResponse.timestamp,
    };
  }
}

This allows new clients to use REST while maintaining SOAP compatibility.

2. Strangler Fig Pattern

Gradually replace SOAP operations with modern alternatives:

  1. Route new operations to REST/gRPC
  2. Keep existing SOAP operations running
  3. Migrate clients incrementally
  4. Decommission SOAP endpoints when usage drops to zero

3. Event-Driven Architecture

Replace synchronous SOAP calls with asynchronous events:

Event-based payment processing
@Injectable()
export class PaymentEventService {
  constructor(
    private readonly eventBus: EventBus,
    private readonly soapService: PaymentService,
  ) {}
 
  async processPaymentAsync(request: ProcessPaymentRequest) {
    // Emit event instead of synchronous SOAP call
    await this.eventBus.publish(
      new PaymentRequestedEvent(request)
    );
  }
 
  @OnEvent('payment.requested')
  async handlePaymentRequested(event: PaymentRequestedEvent) {
    // Process via SOAP in background
    const result = await this.soapService.processPayment(event.request);
    
    // Emit completion event
    await this.eventBus.publish(
      new PaymentCompletedEvent(result)
    );
  }
}

This decouples clients from SOAP's synchronous nature.

Conclusion

SOAP isn't dead—it's just specialized. While REST, GraphQL, and gRPC dominate modern development, SOAP remains essential for enterprise integration, financial services, and systems requiring formal contracts with built-in security and transactions.

The key insights:

SOAP excels when you need guaranteed message delivery, ACID transactions, formal contracts, or integration with existing enterprise systems. Its WS-* standards solve problems that REST developers often reinvent poorly.

SOAP struggles with developer experience, performance, and flexibility. For public APIs, real-time applications, or simple CRUD operations, modern alternatives are better choices.

The NestJS implementation demonstrates that SOAP can coexist with modern frameworks. By understanding SOAP's strengths and limitations, you can make informed architectural decisions rather than dismissing it as legacy technology.

If you're building new systems, start with REST or gRPC. But if you're integrating with banks, healthcare systems, or government agencies, understanding SOAP isn't optional—it's essential.


Related Posts