Expose SOAP Service As REST API -The Serverless Way

By Adrin Mukherjee

Photo by freestocks on Unsplash

In this post, we will go through the steps to expose a SOAP service as a RESTful API using AWS API Gateway and Lambda. The primary driver for such a solution is often incompatibilities of systems involved in the integration. For example: The service client supports REST and JSON, whereas the service provider works solely based on SOAP messages.

To keep things simple, we will refer to a publicly available SOAP based web service accessible here. This calculator service provides simple arithmetic operations like Add, Subtract, Multiply and Divide. We will expose the ‘Add’ operation via a RESTful API. However, this solution could be easily extended to the other operations in the calculator service or to other SOAP services altogether.

Here is a pictorial view of how the solution is perceived to work.

Figure-1: Exposing SOAP service as REST API

The API client places a REST API call to API gateway which has a Lambda back-end. The Lambda function, in turn, transforms JSON request into a SOAP message and interacts with the SOAP service. A reverse path is followed during the response flow.

We will use Node.js to implement the Lambda function and specifically use ‘soap’ library to transform JSON request/response, to/from SOAP. There are of course, other libraries that could be used to achieve the same result. However, this particular library seems to be very simple and yet powerful. The library accepts JavaScript objects and transforms them into SOAP requests, transparently. Needless to say, it also transforms SOAP responses back into plain JavaScript objects.

Working with SOAP requests and responses

Here is a sample source code that leverages the ‘soap’ library, to call the ‘Add’ operation of calculator service. We will refer to this code, in the subsequent sections.

https://github.com/adrin-mukherjee/lambda_rest2soap

Here’s the snippet of ‘Add’ operation along with input/output message structures from the WSDL. Take note of the elements highlighted. They play a significant role in how the corresponding JavaScript objects will have to be formed.

...
<wsdl:types>
<s:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org/">
<s:element name="Add">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="intA" type="s:int"/>
<s:element minOccurs="1" maxOccurs="1" name="intB" type="s:int"/>
</s:sequence>
</s:complexType>
</s:element>
<s:element name="AddResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="AddResult" type="s:int"/>
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</wsdl:types>
...
<wsdl:message name="AddSoapIn">
<wsdl:part name="parameters" element="tns:Add"/>
</wsdl:message>
<wsdl:message name="AddSoapOut">
<wsdl:part name="parameters" element="tns:AddResponse"/></wsdl:message>
...
<wsdl:portType name="CalculatorSoap">
<wsdl:operation name="Add">
<wsdl:input message="tns:AddSoapIn"/>
<wsdl:output message="tns:AddSoapOut"/>
</wsdl:operation>
...

The trick is to understand the relevant operation and input/output structures appearing in WSDL and accordingly form the corresponding JavaScript object structures. We can clearly see that the ‘Add’ operation, uses ‘AddSoapIn’ as input message and ‘AddSoapOut’ as output messages. ‘AddSoapIn’ message leverages the ‘Add’ complex type which has two elements ‘intA’ and ‘intB’. Thus the corresponding JavaScript object structure would be:

{ intA: "<first number>", intB: "<second number>" }

The ‘AddSoapOut’ message leverages ‘AddResponse’ complex type which has ‘AddResult’ element and corresponding JavaScript object structure would be:

{ AddResult: "<result of addition>"}

In request cycle, transformation of a Lambda event into a JavaScript object structure that can be used by the library to generate appropriate SOAP request message (for Add operation), is done by transformRequest function.

const transformRequest = async(event)=>{
let targetRequest = {};
targetRequest.intA = event.a;
targetRequest.intB = event.b;
return targetRequest;
};

Similarly, in the response cycle, transformResponse function is used to work with JavaScript object returned by the library and transform it into response of Lambda function.

const transformResponse = async(targetResponse)=>{
let sourceResponse = {};
sourceResponse.result = targetResponse.AddResult;
return sourceResponse;
};

Working with SOAP bindings and execute the operation

By default, the ‘soap’ library doesn’t generate SOAP1.2 headers. As such, if an operation has a SOAP1.2 binding only, we will have to explicitly mention the same in code using an option that is subsequently passed to createClient call (Refer to source code for details). Also we have to explicitly call the ‘Add’ operation on soap client (Remember, ‘Add’ operation was defined in the WSDL) and pass target request returned by transformRequest function

Here’s a code snippet that creates a soap client and then calls the ‘Add’ operation. The soap client also forces SOAP1.2 headers.

const soap = require('soap');
const SERVICE_WSDL = process.env.SERVICE_WSDL || "http://www.dneonline.com/calculator.asmx?wsdl";
...
var options = {
forceSoap12Headers: true
};
soap.createClient(SERVICE_WSDL, options, function(err, client) {
if(err){
// Handle error
}
client.Add(targetRequest, function(err, result) {
if(err){
// Handle error
}
else{
// Handle result
}
});
});
...

Deploy and test the Lambda

Deploy the Lambda function and test it with the following sample event:

{
"a": "10",
"b": "20"
}

The Lambda should return a response like:

{
"result": 30
}

Note: We will not cover the deployment process of a Lambda function in this post, since there are several good resources available covering that area.

Exposing the Lambda function as a RESTful API is pretty straightforward.

  • Build a REST API from scratch and name it as ‘calculator’
  • Create necessary resources and a GET operation
  • Make sure to select Integration Type as Lambda Function and provide name of Lambda function and click Save. Here will we create a non-proxy Lambda integration.
Figure-2: Create a GET operation with a Non-Proxy Lambda integration
  • Add necessary permissions to Lambda in the next popup and click OK.
Figure-3: Add required permissions to the Lambda function
  • In the Method Execution pane of the GET operation, click on Method Request and under ‘URL Query String Parameters’ section, add two query string parameters- ‘a’ & ‘b’ and mark them as required
Figure-4: Add query string parameters to the API
  • Go back to the Method Execution pane and click on Integration Request.
  • Under ‘Mapping Templates’ section, select ‘Never’ as Request body pass-through mode and add mapping template for application/json content-type.
Figure-5: Create request body mapping template
  • Add the following template in ‘Generate template’ section and click on Save.
{
"a": $input.params('a'),
"b": $input.params('b')
}

Here, essentially we are extracting the values from query-string parameters and creating the necessary JSON request which will be passed as event to Lambda function.

That’s it, we are done. Now let’s deploy the API and test it.

  • From API actions, select Deploy API.
Figure-6: Deploy the API
  • Create a new stage and provide a new name to the stage and click on Deploy.
Figure-7: Create new stage
  • Note the invoke URL of the GET operation from Stages
Figure-8: Copy invoke URL of the GET operation
  • Since we have created a simple GET operation, we can directly point our browser to the invoke URL along with required query string (‘a=<number>&b=<number>’)and check response:
Figure-9: Test the simple GET operation from browser

This serverless solution could be used with any arbitrarily complex SOAP service. However, maintaining the simplicity and maintainability of APIs is a different ballgame and should be dealt with on its own merit.