A Beginner’s Guide to Building a SOAP-Based CRUD API with Python and Spyne

Mário M. Mabande
4 min readJun 16, 2024

--

Remember when we talked about SOAP in “Understanding SOAP: The Old Guard of Web Services”? Today, let’s put theory into practice by building a basic CRUD API using SOAP. We’ll be using Python, our programming language friend.

Let’s Get Started

This project focuses on the essentials of SOAP for API development. To keep things organized and readable, we’ll structure our project using the MVC (Model-View-Controller) pattern. For simplicity, our data will be stored in a JSON file.

In Python, we have a fantastic library called Spyne, It simplifies communication between applications using SOAP protocols. We’ll leverage its capabilities to build our API efficiently.

Why Spyne?

Spyne not only streamlines SOAP integration but also ensures robust communication channels between client and server. Its intuitive design and Pythonic syntax make it a standout choice for projects like ours.

Now, let’s dive into the code and see how Spyne helps us create seamless communication in our SOAP-based CRUD API.

Setting Up the Project

Let’s start by outlining the project structure and the key components involved:

Project Structure

  • app.py: This file initializes the SOAP server using Spyne framework.
  • controllers.py: Contains functions to handle CRUD operations and data persistence.
  • models.py: Defines the data models used in the application.
  • views.py: Implements the SOAP service methods using Spyne decorators.

Requirements

Ensure you have the following Python packages installed, listed in requirements.txt

spyne
zeep
lxml

Implementation Details

User Model and Controller First, let’s define our User and Response model in models.py:

from spyne import Unicode
from spyne.model.complex import ComplexModel, Array

# User model
class User(ComplexModel):
id = Unicode
name = Unicode
email = Unicode
description = Unicode

# Response Message
class ResponseData(ComplexModel):
user = User
users = Array(User)
success = Unicode
message = Unicode

Next, in controllers.py, we implement CRUD functions using JSON file as a storage:

import json
import uuid
from models import User

DATA_FILE = 'users.json'


def load_users():
try:
with open(DATA_FILE, 'r') as file:
return json.load(file)
except FileNotFoundError:
return {}


def save_users(users):
with open(DATA_FILE, 'w') as file:
json.dump(users, file, indent=4)


def add_user(name, email, description):
users = load_users()
user_id = str(uuid.uuid4())
users[user_id] = {
"id": user_id,
"name": name,
"email": email,
"description": description
}
save_users(users)
return user_id


def get_user(user_id):
users = load_users()
return users.get(user_id)


def update_user(user_id, name, email, description):
users = load_users()
if user_id in users:
users[user_id] = {
"id": user_id,
"name": name,
"email": email,
"description": description
}
save_users(users)
return True
return False


def delete_user(user_id):
users = load_users()
if user_id in users:
del users[user_id]
save_users(users)
return True
return False


def list_users():
return list(load_users().values())

SOAP Service Implementation

Now, let’s define our SOAP service methods in views.py using Spyne decorators:

from spyne import ServiceBase, rpc, Unicode, Array
from models import User, ResponseData
import controllers


class UserService(ServiceBase):

@rpc(Unicode, Unicode, Unicode, _returns=Unicode)
def add_user(ctx, name, email, description):
return controllers.add_user(name, email, description)

@rpc(Unicode, _returns=ResponseData)
def get_user(ctx, user_id):
user = controllers.get_user(user_id)
if user:
user = User(
id=user['id'],
name=user['name'],
email=user['email'],
description=user['description']
)
return ResponseData(user=user, success='1', message='User found')
return ResponseData(user=User(), success='0', message='User not found.')

@rpc(_returns=ResponseData)
def get_users(ctx):
users = controllers.list_users()
if users:
users_data = [User(
id=user['id'],
name=user['name'],
email=user['email'],
description=user['description']
) for user in users]
return ResponseData(users=users_data, success='1', message="Users found.")
return ResponseData(users=None, success='0', message="No users found.")

@rpc(Unicode, Unicode, Unicode, Unicode, _returns=ResponseData)
def update_user(ctx, user_id, name, email, description):
success = controllers.update_user(user_id, name, email, description)
message = "User updated successfully" if success else "User not found"
return ResponseData(user=User(), success='1' if success else '0', message=message)

@rpc(Unicode, _returns=ResponseData)
def delete_user(ctx, user_id):
success = controllers.delete_user(user_id)
message = "User deleted successfully" if success else "User not found"
return ResponseData(user=User(), success='1' if success else '0', message=message)

Running the SOAP Server

Finally, in app.py, we setup and run the SOAP server using Spyne and WSGI:

from spyne import Application
from spyne.protocol.soap import Soap11
from spyne.server.wsgi import WsgiApplication
from views import UserService
from wsgiref.simple_server import make_server

# Application setup
application = Application(
[UserService],
tns='api.users',
in_protocol=Soap11(validator='lxml'),
out_protocol=Soap11()
)

# WSGI Application
wsgi_application = WsgiApplication(application)

# Server setup
if __name__ == '__main__':
server = make_server('127.0.0.1', 8000, wsgi_application) # Change to localhost or 127.0.0.1
print("SOAP server running on http://127.0.0.1:8000")
server.serve_forever()

Testing the API with Postman

To interact with our SOAP API, we can use tools like Postman. Here are examples of requests you can make:

Add User (POST)

  • Endpoint: http://localhost:8000
  • Body:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:api="api.users">
<soapenv:Header/>
<soapenv:Body>
<api:add_user>
<name>John Doe</name>
<email>john.doe@example.com</email>
<description>Example user</description>
</api:add_user>
</soapenv:Body>
</soapenv:Envelope>

Response:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:api="api.users">
<soapenv:Body>
<api:add_userResponse>
<return>GeneratedUserUUID</return>
</api:add_userResponse>
</soapenv:Body>
</soapenv:Envelope>

Get User (GET)

  • Endpoint: http://localhost:8000
  • Body:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:api="api.users">
<soapenv:Header/>
<soapenv:Body>
<api:get_user>
<user_id>GeneratedUserUUID</user_id>
</api:get_user>
</soapenv:Body>
</soapenv:Envelope>

Response:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:api="api.users">
<soapenv:Body>
<api:get_userResponse>
<return>
<user>
<id>GeneratedUserUUID</id>
<name>John Doe</name>
<email>john.doe@example.com</email>
<description>Example user</description>
</user>
<success>1</success>
<message>User found</message>
</return>
</api:get_userResponse>
</soapenv:Body>
</soapenv:Envelope>

Conclusion

In this tutorial, we’ve explored how to build a Simple CRUD API using SOAP architecture in Python with Spyne. This approach provides a structured and formal way to communicate between applications, making it suitable for enterprise scenarios.

Bye!

Thank you for reading. Feel free to suggest code improvement on GitHub and comment about this simple implementation. Thanks 🫂

GitHub Link:

https://github.com/mariomthree/crud-soap-api

--

--

Mário M. Mabande
Mário M. Mabande

Written by Mário M. Mabande

Software Engineer dedicated to creating user-centric solutions. Passionate about inclusive projects, especially those empowering people with disabilities.

No responses yet