JavaScript and SOAP were not designed for each other. JSON is JavaScript's native data format; SOAP is XML-based, verbose, namespace-sensitive, and predates the modern web stack by more than a decade. Yet millions of production systems still expose SOAP endpoints, and JavaScript developers still need to talk to them.
This guide covers every practical approach, from raw fetch to dedicated libraries, and explains honestly where each method breaks down.
Why SOAP Is Awkward in JavaScript
Before diving into solutions, it helps to understand the friction points:
- No native SOAP client. Browsers and Node.js have no built-in SOAP support. You're either writing XML by hand or using a library.
- XML namespaces are finicky. SOAP services are strict about namespace URIs. A single misplaced namespace declaration produces a cryptic server fault.
- WSDLs are complex documents. Parsing a WSDL to discover available operations requires significant effort.
- Response parsing is tedious. SOAP responses wrap your data in
<soap:Envelope>and<soap:Body>elements, and nested namespace prefixes make plain XML-to-JSON conversion unreliable.
With those challenges in mind, here are your options.
Option 1: Raw fetch with Manually Constructed XML
The lowest-level approach — useful when you need full control or when library overhead is unacceptable.
async function callGetUser(userId) {
const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://example.com/userservice">
<soap:Body>
<tns:GetUser>
<tns:userId>${userId}</tns:userId>
</tns:GetUser>
</soap:Body>
</soap:Envelope>`;
const response = await fetch("https://api.example.com/UserService", {
method: "POST",
headers: {
"Content-Type": "text/xml;charset=UTF-8",
SOAPAction: '"http://example.com/userservice/GetUser"',
},
body: soapEnvelope,
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const responseText = await response.text();
// Now you need to parse responseText as XML...
return responseText;
}
The problem: Once you have the response text, you still need to parse XML. In the browser, DOMParser works reasonably well. In Node.js, you need a library like fast-xml-parser or xml2js.
Parsing the nested SOAP response is the hard part:
import { XMLParser } from "fast-xml-parser";
const parser = new XMLParser({
ignoreAttributes: false,
removeNSPrefix: true, // strips namespace prefixes
});
const result = parser.parse(responseText);
// Navigate the envelope manually
const user = result["Envelope"]["Body"]["GetUserResponse"]["user"];
This approach is fragile. Namespace prefix names vary between services, response structures change, and error handling for SOAP faults requires a separate code path.
Best for: Simple, single-operation integrations where you control the SOAP service or have very stable contracts.
Option 2: node-soap
node-soap is the most widely used SOAP client for Node.js. It parses WSDL files automatically, generates client methods for each operation, and handles XML serialization and deserialization.
npm install soap
Basic usage:
import soap from "soap";
const WSDL_URL = "https://api.example.com/UserService?wsdl";
const client = await soap.createClientAsync(WSDL_URL);
// Operations become async methods on the client
const [result] = await client.GetUserAsync({ userId: 42 });
console.log(result); // Plain JavaScript object
node-soap also handles WS-Security:
const WSSecurity = soap.WSSecurity;
client.setSecurity(new WSSecurity("username", "password"));
Where it struggles:
- WSDL files with complex inheritance and abstract types sometimes confuse the parser.
- The library's TypeScript types are incomplete; you'll often deal with
any. - Some SOAP services use features like
RPC/encodedstyle, whichnode-soaphandles inconsistently. - Bundle size is significant — it pulls in a lot of XML parsing infrastructure.
Best for: Node.js server-side applications with moderately complex SOAP services where you want automatic type mapping.
Option 3: zeep (Python) or other language-native libraries
If you're in a polyglot environment, sometimes the right answer is to handle the SOAP call in a language with better SOAP tooling. Python's zeep library, Java's JAX-WS, and .NET's System.ServiceModel all have mature, well-tested SOAP clients.
This isn't always practical for a JavaScript team, but it's worth considering if you only have a few SOAP operations to call and the overhead of a small sidecar service is acceptable.
Option 4: Browser-based SOAP with XMLHttpRequest
Before fetch, SOAP calls in browsers used XMLHttpRequest:
function callSoap(url, soapAction, body) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "text/xml;charset=UTF-8");
xhr.setRequestHeader("SOAPAction", soapAction);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseXML);
} else {
reject(new Error(`Request failed: ${xhr.status}`));
}
}
};
xhr.send(body);
});
}
The advantage of XMLHttpRequest is that .responseXML gives you a parsed Document object directly, so you can use DOM methods (querySelector, getElementsByTagNameNS) to navigate the response. This avoids adding an XML parser dependency.
Best for: Browser applications where you're already comfortable with the DOM API and want zero additional dependencies. Not recommended for new code — fetch is the modern standard.
Option 5: A REST Proxy Layer
All of the above options require your JavaScript code to understand SOAP. Every developer on your team needs to know about XML namespaces, SOAP envelopes, WSDLs, and SOAP faults. Updates to the SOAP service may break your namespace assumptions. Testing becomes harder because you need XML fixtures.
A REST proxy layer moves the SOAP complexity out of your application entirely. You register the WSDL URL with a proxy service, and your JavaScript code makes regular JSON API calls:
// Instead of all that XML...
const response = await fetch("https://soapless.miravy.com/api/v1/your-account-id/your-service/GetUser", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.SOAPLESS_API_KEY,
},
body: JSON.stringify({ userId: 42 }),
});
const user = await response.json();
// { id: 42, name: "Alice", email: "alice@acme.com" }
This is what SOAPless provides. You paste your WSDL URL into the dashboard, and within 30 seconds you have REST endpoints that accept JSON and return JSON. The proxy handles envelope construction, namespace management, type marshaling, authentication headers, and SOAP fault translation into standard HTTP status codes.
Your JavaScript code stays clean, testable, and familiar to any developer on the team — no SOAP expertise required.
Choosing Your Approach
| Scenario | Recommended approach |
|---|---|
| One-off script, simple operation | Raw fetch + fast-xml-parser |
| Node.js server, moderate complexity | node-soap |
| Browser app, minimal deps | fetch + DOMParser |
| Team unfamiliar with SOAP | REST proxy (SOAPless) |
| Multiple SOAP services, production use | REST proxy (SOAPless) |
| Need TypeScript safety end-to-end | REST proxy (SOAPless) |
If you're building anything that will be maintained by a team over time, the proxy approach is worth the setup cost. SOAP integration code is notoriously hard to maintain as services evolve, and having a clean JSON interface insulates your application from that volatility.
Whatever approach you choose, logging the raw XML request and response during development will save you significant debugging time when things go wrong.