A practical guide to building adapters that translate between REST APIs and legacy SOAP services in Django with real code, gotchas, and patterns that actually work in production.
Legacy SOAP services are everywhere in enterprise software banks, insurance systems, government portals, ERPs. At some point, you'll be asked to integrate with one. This guide shows you how to wrap SOAP cleanly inside Django so the rest of your system never has to care about XML envelopes.
Understanding the Mismatch
REST and SOAP operate on fundamentally different philosophies. REST is resource-oriented and stateless, using HTTP verbs and JSON. SOAP is operation-oriented, uses XML envelopes, and relies on WSDL contracts. Bridging them means translating not just data formats but also error semantics, authentication schemes, and transport assumptions.
SOAP → REST: Wrapping a SOAP Service with a Django REST API
Step 1 Parse the WSDL
Use the zeep library to parse the WSDL and talk to the SOAP service. Zeep handles envelope construction, type coercion, and namespace gymnastics for you.
pip install zeep djangorestframework# services/soap_client.py
from zeep import Client
from zeep.transports import Transport
from requests import Session
from requests.auth import HTTPBasicAuth
import functools
@functools.lru_cache(maxsize=1)
def get_soap_client() -> Client:
session = Session()
session.auth = HTTPBasicAuth('user', 'secret')
transport = Transport(session=session, timeout=30)
return Client(
wsdl='https://legacy-erp.example.com/service?wsdl',
transport=transport,
)Step 2 Build a Django REST Endpoint
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from zeep.exceptions import Fault
from .soap_client import get_soap_client
class CustomerDetailView(APIView):
def get(self, request, customer_id: str):
client = get_soap_client()
try:
result = client.service.GetCustomerById(
CustomerId=customer_id,
IncludeInactive=False,
)
except Fault as e:
return Response(
{"error": str(e.message)},
status=status.HTTP_502_BAD_GATEWAY,
)
data = {
"id": result.CustomerId,
"name": result.FullName,
"email": result.EmailAddress,
"created_at": result.CreatedDate.isoformat(),
}
return Response(data)REST → SOAP: Receiving REST Requests and Forwarding to SOAP
The reverse is trickier you receive a clean JSON payload, validate it, and construct a SOAP request. The key is treating your Django serializer as the contract layer.
# serializers.py
from rest_framework import serializers
class CreateOrderSerializer(serializers.Serializer):
product_sku = serializers.CharField(max_length=50)
quantity = serializers.IntegerField(min_value=1)
customer_id = serializers.CharField(max_length=36)
shipping_address = serializers.CharField()
# views.py
class CreateOrderView(APIView):
def post(self, request):
serializer = CreateOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
d = serializer.validated_data
client = get_soap_client()
try:
order_ref = client.service.PlaceOrder(
ProductSKU=d['product_sku'],
Quantity=d['quantity'],
CustomerId=d['customer_id'],
ShippingAddress=d['shipping_address'],
)
except Fault as e:
return Response(
{"error": e.message},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
return Response(
{"order_reference": str(order_ref)},
status=status.HTTP_201_CREATED,
)Error Mapping
SOAP Faults map loosely to HTTP status codes. Build a utility to do this translation consistently:
# utils.py
from zeep.exceptions import Fault
from rest_framework import status
SOAP_FAULT_MAP = {
"NOT_FOUND": status.HTTP_404_NOT_FOUND,
"UNAUTHORIZED": status.HTTP_401_UNAUTHORIZED,
"VALIDATION_ERROR": status.HTTP_422_UNPROCESSABLE_ENTITY,
"SERVER_ERROR": status.HTTP_502_BAD_GATEWAY,
}
def soap_fault_to_http(fault: Fault) -> int:
code = getattr(fault, 'code', '') or ''
return SOAP_FAULT_MAP.get(code.upper(), status.HTTP_502_BAD_GATEWAY)Performance: Caching and Async
- Cache WSDL parsing the lru_cache on get_soap_client() means the WSDL is only fetched once per process.
- Use Celery for non-blocking SOAP calls SOAP services can be slow; push fire-and-forget calls to background tasks.
- Cache frequent read responses in Redis with a short TTL SOAP services rarely need to be hit on every request.
The secret to clean SOAP integration is treating the boundary as a strict adapter layer. Keep all SOAP-specific logic behind a service class, expose clean REST contracts outward, and map errors consistently. Your future self will thank you.