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:

  1. Break down the XML structure to YAML or JSON.
  2. Rewrite it using a templating structure like OpenAPI 3.1.
  3. Write the Infrastructure as Code (IaC) to redeploy the new endpoint.

Or:

  1. 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.