A single runnable repository that implements five API communication styles against the same domain (a books catalog), so you can compare them side-by-side.
┌──────────────┬──────────────────────────────────────────┐
│ Style │ One-line summary │
├──────────────┼──────────────────────────────────────────┤
│ REST │ Resource URLs + HTTP verbs │
│ GraphQL │ One endpoint, client picks fields │
│ gRPC │ Binary (Protobuf) + HTTP/2 │
│ SOAP │ XML envelopes + strict contracts │
│ WebSocket │ Persistent bi-directional connection │
└──────────────┴──────────────────────────────────────────┘
Every style exposes the same four operations:
list_books · get_book(id) · create_book · search_books(q)
# 1. Clone / enter the repo
cd api-styles-python
# 2. Install all dependencies (one command)
pip install -r requirements.txt
# 3. Run any style — two terminals needed
python rest/server.py # terminal 1 — starts server
python rest/client.py # terminal 2 — runs demo + timing
# Replace "rest" with graphql_api / grpc_api / soap_api / websocket_apiRun all five + benchmark in one go:
python benchmark/run_all.pyapi-styles-python/
├── requirements.txt
├── common/
│ └── data.py 20 seed books shared by every server
├── rest/
│ ├── server.py FastAPI on port 8001
│ └── client.py httpx + timing
├── graphql_api/
│ ├── server.py Strawberry + FastAPI on port 8002
│ └── client.py raw HTTP POST + timing + field-selection demo
├── grpc_api/
│ ├── proto/books.proto Protobuf schema
│ ├── books_pb2.py generated stubs (committed)
│ ├── books_pb2_grpc.py generated stubs (committed)
│ ├── server.py grpcio server on port 50051
│ └── client.py grpc channel + streaming + timing
├── soap_api/
│ ├── server.py Spyne WSGI on port 8003
│ └── client.py zeep + raw XML display + timing
├── websocket_api/
│ ├── server.py FastAPI WebSocket on port 8004
│ └── client.py websockets lib + push subscription demo
└── benchmark/
├── run_all.py starts all servers, benchmarks, prints table
└── sample_results.md pre-recorded results with explanations
REST (Representational State Transfer) maps every noun in your system to a URL and uses HTTP verbs to express operations:
| Verb | Meaning | Example |
|---|---|---|
| GET | Read | GET /books |
| POST | Create | POST /books |
| PUT | Replace | PUT /books/5 |
| DELETE | Delete | DELETE /books/5 |
Responses are plain JSON over HTTP/1.1. Stateless — every request carries all needed context.
- Public-facing web or mobile APIs consumed by browsers/apps you don't control
- CRUD services where resources map naturally to URLs
- Teams that need maximum tooling support (Swagger, Postman, curl)
- Any situation where simplicity matters more than raw performance
- You need real-time push (use WebSocket)
- Internal microservices where latency and payload size matter (use gRPC)
- You need strict typed contracts for enterprise integration (consider SOAP)
- Mobile clients on metered connections fetching large objects (consider GraphQL)
# Terminal 1
python rest/server.py
# → http://127.0.0.1:8001
# Terminal 2
python rest/client.py# List all books
curl http://127.0.0.1:8001/books
# Get book by ID
curl http://127.0.0.1:8001/books/3
# Search
curl "http://127.0.0.1:8001/books?q=python"
# Create
curl -X POST http://127.0.0.1:8001/books \
-H "Content-Type: application/json" \
-d '{"title":"New Book","author":"Me","year":2024,"pages":300,"genre":"Tech"}'GET /books/1
{
"id": 1,
"title": "Clean Code",
"author": "Robert Martin",
"year": 2008,
"pages": 431,
"genre": "Engineering"
}GraphQL exposes a single endpoint (POST /graphql). The client sends a query document describing exactly which fields it needs. The server returns only those fields — no more, no less.
Client sends: Server returns:
{ {
books { → "data": {
id "books": [
title {"id":1, "title":"Clean Code"},
} {"id":2, "title":"The Pragmatic Programmer"},
} ...
}
}
- Mobile apps or IoT devices on limited bandwidth — request only what you render
- Frontend teams that iterate fast and don't want a new REST endpoint for every screen
- APIs that aggregate data from multiple backend sources (avoids N+1 HTTP round-trips)
- When different clients (web/mobile/TV) need different field subsets of the same data
- Simple CRUD with a fixed client — REST is simpler
- File uploads (awkward in GraphQL)
- Clients you don't control (public API) — query complexity becomes a security concern
- Teams unfamiliar with it — there is real operational overhead (query depth limiting, persisted queries, etc.)
# Terminal 1
python graphql_api/server.py
# → http://127.0.0.1:8002/graphql
# Interactive playground: http://127.0.0.1:8002/graphql
# Terminal 2
python graphql_api/client.py# List all books (all fields)
curl -X POST http://127.0.0.1:8002/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ books { id title author year pages genre } }"}'
# List books — id and title ONLY (smaller payload)
curl -X POST http://127.0.0.1:8002/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ books { id title } }"}'
# Search
curl -X POST http://127.0.0.1:8002/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ searchBooks(q: \"python\") { id title author } }"}'
# Create (mutation)
curl -X POST http://127.0.0.1:8002/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createBook(book:{title:\"Test\",author:\"A\",year:2024,pages:100,genre:\"Tech\"}) { id title } }"}'Full payload (6 fields × 20 books): ~1,842 bytes
Slim payload (id + title only) : ~418 bytes ← 4.4× smaller
The client decides — no new endpoint needed.
gRPC uses HTTP/2 for transport and Protocol Buffers (Protobuf) for serialization. Instead of text JSON, messages are binary-encoded using a schema (.proto file). Field names are replaced by small integers on the wire — payloads are 5–10× smaller than JSON.
HTTP/2 multiplexes multiple calls over one TCP connection and supports four call patterns:
- Unary (one request → one response) — like REST
- Server streaming — server sends a stream of messages
- Client streaming — client sends a stream of messages
- Bidirectional streaming — both sides stream simultaneously
- Internal microservice communication (service mesh, Kubernetes)
- When payload size and latency are critical constraints
- Polyglot environments — Protobuf generates client code for 10+ languages
- Streaming large datasets or live feeds between services
- Strong schema enforcement is required between teams
- Browser clients — gRPC requires gRPC-Web proxy in browsers (HTTP/2 framing not accessible to browser JS)
- Public APIs — Protobuf is not human-readable; debugging is harder
- Teams without DevOps tooling for Protobuf schema versioning
- Simple low-traffic APIs where REST simplicity wins
# Terminal 1
python grpc_api/server.py
# → 127.0.0.1:50051
# Terminal 2
python grpc_api/client.pyservice BookService {
rpc ListBooks (Empty) returns (BookList);
rpc GetBook (BookId) returns (Book);
rpc CreateBook (Book) returns (Book);
rpc SearchBooks (SearchQuery) returns (BookList);
rpc StreamBooks (Empty) returns (stream Book); // server-streaming
}import grpc
import grpc_api.books_pb2 as pb2
import grpc_api.books_pb2_grpc as pb2_grpc
with grpc.insecure_channel("127.0.0.1:50051") as channel:
stub = pb2_grpc.BookServiceStub(channel)
# Unary call
books = stub.ListBooks(pb2.Empty())
print(books.books[0].title)
# Server streaming
for book in stub.StreamBooks(pb2.Empty()):
print(book.title)JSON (REST) : 1,842 bytes for 20 books
Protobuf (gRPC): 312 bytes for 20 books ← 5.9× smaller
SOAP (Simple Object Access Protocol) wraps every call in an XML envelope. The server publishes a WSDL (Web Services Description Language) file — a machine-readable contract that describes every method, parameter, and return type. Clients generate typed proxies from the WSDL.
<!-- Every SOAP request looks like this -->
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<tns:get_book>
<tns:book_id>5</tns:book_id>
</tns:get_book>
</soap:Body>
</soap:Envelope>- Integrating with banks, insurance companies, government agencies, or any legacy enterprise system that exposes SOAP endpoints
- You need WS-Security (XML signatures, encryption)
- You need ACID-compliant distributed transactions (WS-AtomicTransaction)
- Strict formal contracts are required between organizations
- Building a new API from scratch — SOAP's verbosity and complexity are rarely worth it for greenfield projects
- Mobile or browser clients — XML parsing is expensive on constrained devices
- Any performance-sensitive path
# Terminal 1
python soap_api/server.py
# → http://127.0.0.1:8003
# WSDL at: http://127.0.0.1:8003/books?wsdl
# Terminal 2
python soap_api/client.pyfrom zeep import Client
client = Client("http://127.0.0.1:8003/books?wsdl")
books = client.service.list_books()
book = client.service.get_book(book_id=3)
result = client.service.search_books(q="python")
new = client.service.create_book(
title="SOAP Book", author="A", year=2000, pages=100, genre="Legacy"
)REST JSON : 1,842 bytes
SOAP XML : 5,893 bytes ← 3.2× larger (XML tags, namespaces, envelope overhead)
WebSocket upgrades a normal HTTP connection into a persistent, full-duplex TCP tunnel. Once the handshake completes, either side can send frames at any time with minimal overhead (2–14 byte frame header vs hundreds of bytes of HTTP headers).
Client Server
|--- HTTP Upgrade -------->|
|<-- 101 Switching --------|
|<========================>| (persistent connection)
|--- {"action":"list"} --->|
|<--- {"data":[...]} ------|
|<--- {"event":"push"} ----| (server-initiated)
|<--- {"event":"push"} ----|
- Real-time applications: chat, collaborative editing, live dashboards
- Push notifications — server needs to push data without the client polling
- Multiplayer games or trading platforms where every millisecond counts
- Any scenario where you'd otherwise poll a REST API repeatedly
- Standard CRUD APIs — HTTP is simpler and better cached
- One-shot requests — the connection overhead isn't worth it for a single request
- Clients behind proxies/firewalls that strip non-HTTP traffic
# Terminal 1
python websocket_api/server.py
# → ws://127.0.0.1:8004/ws/books
# Terminal 2
python websocket_api/client.pyimport asyncio, json, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8004/ws/books") as ws:
# Request-response
await ws.send(json.dumps({"action": "list_books"}))
books = json.loads(await ws.recv())["data"]
print(f"{len(books)} books received")
# Subscribe to server push (500 ms interval)
await ws.send(json.dumps({"action": "subscribe", "interval_ms": 500}))
for _ in range(6): # receive 6 pushed events
event = json.loads(await ws.recv())
print(event)
asyncio.run(main())| Criterion | REST | GraphQL | gRPC | SOAP | WebSocket |
|---|---|---|---|---|---|
| Browser support | ✅ | ✅ | ✅ | ✅ | |
| Human-readable wire | ✅ | ✅ | ❌ | ✅ | |
| Server push | ❌ | ❌ | ✅* | ❌ | ✅ |
| Field selection | ❌ | ✅ | ❌ | ❌ | ❌ |
| Small payload | ✅ | ✅ | ❌ | ||
| Low latency | ✅ | ❌ | ✅ | ||
| Strict contract (WSDL) | ❌ | ✅ | ✅ | ❌ | |
| Tooling / ecosystem | ✅ | ✅ | ✅ | ||
| Learning curve | Low | Medium | Medium | High | Medium |
✅ strong ·
See benchmark/sample_results.md for pre-recorded numbers with explanations.
Run them yourself:
python benchmark/run_all.pyStyle Mean (ms) P95 (ms) P99 (ms) Payload (B)
---------------------------------------------------------
REST 2.14 3.41 5.12 1,842
GraphQL 2.87 4.23 6.30 1,842
gRPC 0.91 1.38 1.97 312 ← binary Protobuf
SOAP 4.73 7.09 10.18 5,893 ← XML overhead
WebSocket 0.38 0.57 0.84 1,842 ← persistent conn
Key insights:
- gRPC payload is 5.9× smaller than REST/GraphQL/WebSocket
- WebSocket per-message latency is lowest because TCP handshake is paid once
- SOAP is slowest due to XML parsing on both sides
- GraphQL payload drops to ~418 bytes when you request only
id + title
| Style | Command | Port | Health check |
|---|---|---|---|
| REST | python rest/server.py |
8001 | curl http://127.0.0.1:8001/health |
| GraphQL | python graphql_api/server.py |
8002 | curl http://127.0.0.1:8002/health |
| gRPC | python grpc_api/server.py |
50051 | (use grpc_api/client.py) |
| SOAP | python soap_api/server.py |
8003 | curl http://127.0.0.1:8003/books?wsdl |
| WebSocket | python websocket_api/server.py |
8004 | curl http://127.0.0.1:8004/health |
fastapi REST + GraphQL + WebSocket server framework
uvicorn ASGI server
strawberry-graphql GraphQL schema builder
grpcio gRPC runtime
grpcio-tools protoc compiler (generates books_pb2*.py from .proto)
spyne SOAP server framework
lxml XML parser (required by Spyne)
zeep SOAP client
websockets WebSocket client library
httpx HTTP client (REST + GraphQL clients)
Install everything with:
pip install -r requirements.txtTo regenerate gRPC stubs from the proto (only needed if you edit books.proto):
python -m grpc_tools.protoc \
-I grpc_api/proto \
--python_out=grpc_api \
--grpc_python_out=grpc_api \
grpc_api/proto/books.proto