SOAP to REST API using Amazon Q
I’ll say it here first: this approach can work with basically any LLM, but in my scenario, I like playing with Amazon Q—both the Developer and the Developer Chat versions.
The Scenario
We have a SOAP API that needs modernization; in other words, we need to get rid of it and transition to a REST API. Here’s what the SOAP API looks like:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://example.com/soap-api"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/soap-api"
name="WebService">
<!-- Types Definition -->
<types>
<xsd:schema targetNamespace="http://example.com/soap-api">
<!-- Request and Response Structures -->
<xsd:element name="GetRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ResourceID" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ResourceData" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="PutRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ResourceID" type="xsd:string"/>
<xsd:element name="ResourceData" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="PutResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Success" type="xsd:boolean"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="PatchRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ResourceID" type="xsd:string"/>
<xsd:element name="PartialData" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="PatchResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Success" type="xsd:boolean"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="DeleteRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ResourceID" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="DeleteResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Success" type="xsd:boolean"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</types>
<!-- Message Definitions -->
<message name="GetRequestMessage">
<part name="parameters" element="tns:GetRequest"/>
</message>
<message name="GetResponseMessage">
<part name="parameters" element="tns:GetResponse"/>
</message>
<message name="PutRequestMessage">
<part name="parameters" element="tns:PutRequest"/>
</message>
<message name="PutResponseMessage">
<part name="parameters" element="tns:PutResponse"/>
</message>
<message name="PatchRequestMessage">
<part name="parameters" element="tns:PatchRequest"/>
</message>
<message name="PatchResponseMessage">
<part name="parameters" element="tns:PatchResponse"/>
</message>
<message name="DeleteRequestMessage">
<part name="parameters" element="tns:DeleteRequest"/>
</message>
<message name="DeleteResponseMessage">
<part name="parameters" element="tns:DeleteResponse"/>
</message>
<!-- Port Type Definition -->
<portType name="WebServicePortType">
<operation name="GetResource">
<input message="tns:GetRequestMessage"/>
<output message="tns:GetResponseMessage"/>
</operation>
<operation name="PutResource">
<input message="tns:PutRequestMessage"/>
<output message="tns:PutResponseMessage"/>
</operation>
<operation name="PatchResource">
<input message="tns:PatchRequestMessage"/>
<output message="tns:PatchResponseMessage"/>
</operation>
<operation name="DeleteResource">
<input message="tns:DeleteRequestMessage"/>
<output message="tns:DeleteResponseMessage"/>
</operation>
</portType>
<!-- Binding Definition -->
<binding name="WebServiceBinding" type="tns:WebServicePortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="GetResource">
<soap:operation soapAction="http://example.com/soap-api/GetResource"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
<operation name="PutResource">
<soap:operation soapAction="http://example.com/soap-api/PutResource"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
<operation name="PatchResource">
<soap:operation soapAction="http://example.com/soap-api/PatchResource"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
<operation name="DeleteResource">
<soap:operation soapAction="http://example.com/soap-api/DeleteResource"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<!-- Service Definition -->
<service name="WebService">
<port name="WebServicePort" binding="tns:WebServiceBinding">
<soap:address location="http://example.com/soap-api/WebService"/>
</port>
</service>
</definitions>
Now, to me and a lot of you reading this, you’ll have one of two responses: “Why am I reading XML in 2024?” or “SOAP really should be dead in 2024.” No matter which camp you’re in, there’s a way to get past this. You can:
- Break down the XML structure to YAML or JSON.
- Rewrite it using a templating structure like OpenAPI 3.1.
- Write the Infrastructure as Code (IaC) to redeploy the new endpoint.
Or:
- Ask an LLM to do it.
Let’s see the results.
Leveraging Amazon Q for the Rewrite
Opening up Amazon Q and logging into our Builder accounts, we ask:
Change this SOAP spec to REST in YAML, please.
We say “please” in case of sentience one day, and the new LLMs see that we asked nicely, not rudely (I’m looking at you, Boston Dynamics, and your robot-kicking videos).
From here, Q spits out a response like:
openapi: 3.0.0
info:
title: WebService API
description: A RESTful API for managing resources, simulating GET, PUT, PATCH, and DELETE operations.
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/resources/{id}:
get:
summary: Get a resource
description: Retrieve a resource by its ID.
parameters:
- in: path
name: id
required: true
schema:
type: string
description: The ID of the resource to retrieve.
responses:
'200':
description: A resource object.
content:
application/json:
schema:
type: object
properties:
id:
type: string
resourceData:
type: string
'404':
description: Resource not found.
put:
summary: Create or update a resource
description: Create a new resource or update an existing resource by its ID.
parameters:
- in: path
name: id
required: true
schema:
type: string
description: The ID of the resource to create or update.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
resourceData:
type: string
responses:
'200':
description: Successfully created or updated the resource.
'400':
description: Invalid request parameters.
patch:
summary: Partially update a resource
description: Partially update a resource by its ID.
parameters:
- in: path
name: id
required: true
schema:
type: string
description: The ID of the resource to update.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
partialData:
type: string
responses:
'200':
description: Successfully updated the resource.
'400':
description: Invalid request parameters.
delete:
summary: Delete a resource
description: Delete a resource by its ID.
parameters:
- in: path
name: id
required: true
schema:
type: string
description: The ID of the resource to delete.
responses:
'200':
description: Successfully deleted the resource.
'404':
description: Resource not found.
components:
schemas:
Resource:
type: object
properties:
id:
type: string
resourceData:
type: string
PartialResource:
type: object
properties:
partialData:
type: string
Here we get an OpenAPI spec and a little explainer. Nice to know and have—can’t complain there.
Building the IaC
Now let’s get it to do the fun bit—build our IaC.
What we choose here is subjective. I’m more of a CDK user/obsessive, but in testing, this also worked if you’re more Terraform-inclined as well.
We prompt Q again:
Create TypeScript AWS CDK based on the last response, creating an API Gateway to use that spec.
Here’s where it takes a curious route. The LLM knows that we have a YAML file and is aware that with CDK constructs, you can inject the YAML file into the apigateway.SpecRestApi
, meeting the minimum requirements to achieve our goals. But for me, this is somewhat of a fail. If we really want to take advantage of IaC, we would want more of a rewrite. So, not to dawdle any longer, let’s give it another prompt:
Convert the openapi.yml to CDK resources utilizing CDK as a whole with no import of API definition.
And the results are… pretty nice, actually:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class ApiGatewayOpenapiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Define a Lambda function to handle requests (Placeholder)
const apiHandler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_18_X,
code: lambda.Code.fromInline(`
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello from API Gateway!" }),
};
};
`),
handler: 'index.handler',
});
// Create the API Gateway
const api = new apigateway.RestApi(this, 'WebServiceAPI', {
restApiName: 'WebService API',
description: 'An API for managing resources.',
deployOptions: {
stageName: 'v1',
},
});
// Define the /resources/{id} resource
const resources = api.root.addResource('resources');
const resourceById = resources.addResource('{id}');
// Add GET method
resourceById.addMethod('GET', new apigateway.LambdaIntegration(apiHandler), {
requestParameters: {
'method.request.path.id': true,
},
});
// Add PUT method
resourceById.addMethod('PUT', new apigateway.LambdaIntegration(apiHandler), {
requestParameters: {
'method.request.path.id': true,
},
requestModels: {
'application/json': new apigateway.Model(this, 'PutRequestModel', {
restApi: api,
contentType: 'application/json',
modelName: 'PutRequestModel',
schema: {
type: apigateway.JsonSchemaType.OBJECT,
properties: {
resourceData: { type: apigateway.JsonSchemaType.STRING },
},
},
}),
},
});
// Add PATCH method
resourceById.addMethod('PATCH', new apigateway.LambdaIntegration(apiHandler), {
requestParameters: {
'method.request.path.id': true,
},
requestModels: {
'application/json': new apigateway.Model(this, 'PatchRequestModel', {
restApi: api,
contentType: 'application/json',
modelName: 'PatchRequestModel',
schema: {
type: apigateway.JsonSchemaType.OBJECT,
properties: {
partialData: { type: apigateway.JsonSchemaType.STRING },
},
},
}),
},
});
// Add DELETE method
resourceById.addMethod('DELETE', new apigateway.LambdaIntegration(apiHandler), {
requestParameters: {
'method.request.path.id': true,
},
});
// Output the API Gateway URL
new cdk.CfnOutput(this, 'ApiUrl', {
value: api.url,
});
}
}
Final Thoughts
With some contextual awareness, we can pick up a few faults: the IaC structure is raw, and the naming conventions aren’t really polished. But we just spent 10 minutes for a quick conversion of APIs. Now that valuable time can be used elsewhere.
Don’t treat LLMs like a job replacement; they’re here to make our lives better if used right.