REST APIs use HTTP verbs to express intent: GET retrieves data, POST creates resources, PUT replaces them, PATCH updates them partially, and DELETE removes them. Zato lets you handle each verb with a dedicated method, so one service can respond differently depending on how it's called.
Instead of checking the HTTP method inside handle(), you implement handle_GET, handle_POST, and so on. Zato routes requests to the right method automatically, and returns 405 Method Not Allowed if a client uses a verb you haven't implemented.
# -*- coding: utf-8 -*-
# stdlib
from http import HTTPStatus
# Zato
from zato.server.service import Service
class CustomerResource(Service):
name = 'customer.resource'
def handle_GET(self):
customer_id = self.request.http.params.get('customer_id')
customer = self.invoke('customer.get-by-id', customer_id=customer_id)
self.response.payload = customer
def handle_DELETE(self):
customer_id = self.request.http.params.get('customer_id')
self.invoke('customer.delete', customer_id=customer_id)
self.response.status_code = HTTPStatus.NO_CONTENT
Mount this on a channel at /api/customers/{customer_id} and clients can:
# Get a customer
curl http://localhost:11223/api/customers/CUST-001
# Delete a customer
curl -X DELETE http://localhost:11223/api/customers/CUST-001
# Try POST - returns 405 Method Not Allowed
curl -X POST http://localhost:11223/api/customers/CUST-001
A typical REST resource supports all CRUD operations:
# -*- coding: utf-8 -*-
# stdlib
from http import HTTPStatus
# Zato
from zato.server.service import Service
class OrderResource(Service):
name = 'order.resource'
def handle_GET(self):
""" Retrieves an order by ID.
"""
order_id = self.request.http.params.get('order_id')
order = self.invoke('order.get-by-id', order_id=order_id)
if not order:
self.response.status_code = HTTPStatus.NOT_FOUND
self.response.payload = {'error': 'Order not found'}
return
self.response.payload = order
def handle_POST(self):
""" Creates a new order.
"""
data = self.request.payload
new_order = self.invoke('order.create', data=data)
self.response.status_code = HTTPStatus.CREATED
self.response.headers['Location'] = f'/api/orders/{new_order["id"]}'
self.response.payload = new_order
def handle_PUT(self):
""" Replaces an entire order.
"""
order_id = self.request.http.params.get('order_id')
data = self.request.payload
updated = self.invoke('order.replace', order_id=order_id, data=data)
self.response.payload = updated
def handle_PATCH(self):
""" Partially updates an order.
"""
order_id = self.request.http.params.get('order_id')
changes = self.request.payload
updated = self.invoke('order.update', order_id=order_id, changes=changes)
self.response.payload = updated
def handle_DELETE(self):
""" Deletes an order.
"""
order_id = self.request.http.params.get('order_id')
self.invoke('order.delete', order_id=order_id)
self.response.status_code = HTTPStatus.NO_CONTENT
Test all the verbs:
# Create
curl -X POST http://localhost:11223/api/orders \
-d '{"customer_id": "CUST-001", "items": [{"sku": "ABC", "qty": 2}]}'
# Read
curl http://localhost:11223/api/orders/ORD-123
# Update (partial)
curl -X PATCH http://localhost:11223/api/orders/ORD-123 \
-d '{"status": "shipped"}'
# Replace (full)
curl -X PUT http://localhost:11223/api/orders/ORD-123 \
-d '{"customer_id": "CUST-001", "items": [{"sku": "XYZ", "qty": 5}], "status": "pending"}'
# Delete
curl -X DELETE http://localhost:11223/api/orders/ORD-123
Often you need two endpoints: one for the collection (/api/orders) and one for individual items (/api/orders/{id}). You can use separate services or handle both in one:
# -*- coding: utf-8 -*-
# stdlib
from http import HTTPStatus
# Zato
from zato.server.service import Service
class EmployeeAPI(Service):
name = 'employee.api'
def handle_GET(self):
employee_id = self.request.http.params.get('employee_id')
if employee_id:
# Single employee
employee = self.invoke('employee.get', employee_id=employee_id)
self.response.payload = employee
else:
# List all employees
department = self.request.http.params.get('department')
employees = self.invoke('employee.list', department=department)
self.response.payload = {'employees': employees}
def handle_POST(self):
# POST only makes sense for collections, not individual items
data = self.request.payload
new_employee = self.invoke('employee.create', data=data)
self.response.status_code = HTTPStatus.CREATED
self.response.payload = new_employee
For completeness, you can also handle HEAD (metadata without body) and OPTIONS (supported methods):
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class DocumentResource(Service):
name = 'document.resource'
def handle_GET(self):
doc_id = self.request.http.params.get('doc_id')
document = self.invoke('document.get', doc_id=doc_id)
self.response.payload = document
def handle_HEAD(self):
# Return headers only, no body - useful for checking if resource exists
doc_id = self.request.http.params.get('doc_id')
exists = self.invoke('document.exists', doc_id=doc_id)
if not exists:
self.response.status_code = 404
def handle_OPTIONS(self):
# Tell clients what methods are available
self.response.headers['Allow'] = 'GET, HEAD, OPTIONS, DELETE'
handle_VERB methods, Zato ignores the handle() method