Migration & Upgrades

Migrating from SOAP to REST APIs

Strategy for replacing SOAP/XML web services with REST/JSON APIs — WSDL analysis, endpoint mapping, error code translation, and backward compatibility.

SOAP vs REST: The Core Differences

SOAP (Simple Object Access Protocol) and REST (Representational State Transfer) represent fundamentally different philosophies about API design. Understanding the conceptual differences is essential before planning a migration.

AspectSOAPREST
**Protocol**Protocol (strict spec)Architectural style (flexible)
**Transport**HTTP, SMTP, JMS, TCPHTTP only (in practice)
**Data format**XML (mandatory)JSON, XML, CSV, any
**Contract**WSDL (machine-readable)OpenAPI/Swagger (convention)
**Error handling**SOAP Fault envelopeHTTP status codes + body
**Security**WS-Security (headers)OAuth 2.0, API keys, JWT
**Caching**Not cacheable by defaultHTTP caching (GET requests)
**State**Often stateful (WS-*)Stateless (per request)

When Migration Makes Sense

  • New clients are mobile/JavaScript apps that work better with JSON
  • SOAP overhead (XML parsing, WS-* stack) is a performance bottleneck
  • You want to expose your API as a public developer platform
  • Existing SOAP clients are being retired or can be updated

When to Keep SOAP

  • Enterprise B2B integrations with contractual WSDL requirements
  • High-security government or financial systems using WS-Security
  • Reliable messaging requirements (WS-ReliableMessaging)
  • Partners cannot update their systems

Step 1: WSDL Analysis

The WSDL (Web Services Description Language) document is your migration blueprint. It formally defines every operation, data type, and fault your SOAP service exposes.

# Fetch the WSDL:
curl https://your-soap-service.com/service?wsdl > service.wsdl

# Install xmllint for analysis:
sudo apt install libxml2-utils

# List all operations (methods) in the WSDL:
xmllint --xpath '//wsdl:operation/@name' --noout service.wsdl 2>/dev/null

# Or with Python lxml:
python3 - << 'EOF'
from lxml import etree
tree = etree.parse('service.wsdl')
ns = {'wsdl': 'http://schemas.xmlsoap.org/wsdl/'}
ops = tree.xpath('//wsdl:operation/@name', namespaces=ns)
for op in sorted(set(ops)):
    print(op)
EOF

Mapping SOAP Operations to REST Endpoints

SOAP operations are verbs; REST uses HTTP verbs + resource nouns:

SOAP OperationREST EndpointHTTP MethodNotes
`GetUser(userId)``/users/{id}`GETRead, cacheable
`CreateUser(userData)``/users`POSTReturns 201 Created
`UpdateUser(userId, data)``/users/{id}`PUT or PATCHFull vs partial
`DeleteUser(userId)``/users/{id}`DELETEReturns 204
`SearchUsers(criteria)``/users?q=...`GETQuery params
`ProcessOrder(order)``/orders`POSTAction as resource
`GetOrderStatus(orderId)``/orders/{id}/status`GETSub-resource

For SOAP operations that are genuinely RPC-like (not CRUD), treat the action as a resource: CalculateShipping(items)POST /shipping-quotes.

Step 2: Error Code Translation

SOAP Fault Structure

SOAP uses a uniform envelope for all errors regardless of the error type:

<!-- SOAP Fault Response (always HTTP 500 in basic SOAP!) -->
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <soap:Fault>
      <faultcode>soap:Client</faultcode>      <!-- Client or Server -->
      <faultstring>User not found</faultstring>
      <detail>
        <error code="USER_NOT_FOUND" />
      </detail>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>

Mapping to HTTP Status Codes

SOAP FaultSOAP `faultcode`REST HTTP StatusNotes
Input validation failure`soap:Client`400 Bad RequestClient's fault
Authentication failed`soap:Client`401 UnauthorizedAdd WWW-Authenticate
Insufficient permissions`soap:Client`403 ForbiddenAuth OK, access denied
Resource not found`soap:Client`404 Not Found
Business rule violation`soap:Client`422 Unprocessable EntitySemantic error
Rate limit exceeded`soap:Client`429 Too Many RequestsAdd Retry-After
Server-side error`soap:Server`500 Internal Server Error
Service unavailable`soap:Server`503 Service UnavailableAdd Retry-After

REST Error Response Format (RFC 7807 Problem Details)

{
  "type": "https://api.your-domain.com/errors/user-not-found",
  "title": "User Not Found",
  "status": 404,
  "detail": "No user with ID 42 exists in the system.",
  "instance": "/users/42"
}

Step 3: Migration Strategy

Strangler Fig Pattern

The strangler fig pattern (Martin Fowler) migrates incrementally without a big-bang cutover:

                    ┌──────────────────────────────┐
                    │         API Gateway           │
                    └──────────────────────────────┘
                         │              │
               ┌─────────┘              └────────────┐
               ▼                                     ▼
   ┌──────────────────────┐          ┌──────────────────────┐
   │  Legacy SOAP Service │          │  New REST Service     │
   │  (being strangled)   │          │  (growing)            │
   └──────────────────────┘          └──────────────────────┘

Implementation with nginx as the routing layer:

# Route to REST for new endpoints, SOAP for legacy:
location /api/v2/ {
    proxy_pass http://rest_service;
}

location /services/ {
    # SOAP → REST adapter (translation layer)
    proxy_pass http://soap_adapter;
}

SOAP Adapter (Translation Shim)

Build a thin adapter that accepts SOAP requests from legacy clients and calls the new REST service internally:

from fastapi import FastAPI, Request
from lxml import etree
import httpx

app = FastAPI()

@app.post('/services/UserService')
async def soap_adapter(request: Request) -> str:
    body = await request.body()
    tree = etree.fromstring(body)

    ns = {'soap': 'http://schemas.xmlsoap.org/soap/envelope/'}
    op = tree.xpath('//soap:Body/*[1]', namespaces=ns)[0]
    operation = op.tag.split('}')[1]  # Strip namespace

    if operation == 'GetUser':
        user_id = op.find('userId').text  # type: ignore
        async with httpx.AsyncClient() as client:
            resp = await client.get(f'http://rest-service/users/{user_id}')
        user = resp.json()
        return f'''<soap:Envelope xmlns:soap="...">
          <soap:Body><GetUserResponse>
            <id>{user["id"]}</id>
            <name>{user["name"]}</name>
          </GetUserResponse></soap:Body></soap:Envelope>'''
    raise ValueError(f'Unknown operation: {operation}')

Step 4: Client Communication Plan

Versioned Endpoint Dual Availability

Maintain both API versions simultaneously during transition:

POST /services/UserService      # SOAP (legacy, deprecated)
GET  /api/v2/users/{id}         # REST (new)

Add deprecation notices to SOAP responses:

<!-- In SOAP response header during deprecation period: -->
<soap:Header>
  <Deprecation>true</Deprecation>
  <Sunset>2026-06-01</Sunset>
  <Link href="https://api.your-domain.com/docs/v2" rel="successor-version"/>
</soap:Header>

Migration Timeline Template

Month 1-2:   REST API v2 in beta — invite early adopters
Month 3:     REST API v2 stable — announce SOAP deprecation
Month 4-5:   SOAP endpoints return Deprecation header
Month 6:     SOAP endpoints begin returning 410 Gone for migrated operations
Month 7+:    Decommission SOAP infrastructure

Communicate proactively: send deprecation notices via email to registered API consumers, post migration guides in your developer portal, and offer migration office hours for large enterprise clients who need hands-on help.

Related Protocols

Related Glossary Terms

More in Migration & Upgrades