HTTP verbs in REST services
Verb-specific handlers - one service responding differently to GET, POST, PUT, PATCH and DELETE.
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. You can handle each verb with a dedicated method, so one service responds differently depending on how it is called.
Instead of checking the HTTP method inside handle, implement handle_GET, handle_POST and so on. Zato routes each request to the matching method and answers 405 Method Not Allowed when a client uses a verb the service does not implement. A service with verb-specific handlers never runs its handle method for REST requests.
GET and DELETE in one service
The service below reads a customer on GET and removes one on DELETE:
# -*- coding: utf-8 -*-
# stdlib
from http import HTTPStatus
# Zato
from zato.server.service import Service
class CustomerResource(Service):
""" Reads and deletes customers by their ID.
"""
name = 'demo.rest.customer-resource'
def handle_GET(self) -> 'None':
customer_id = self.request.http.params['customer_id']
customer = self.invoke('customer.get-by-id', customer_id=customer_id)
self.response.payload = customer
def handle_DELETE(self) -> 'None':
customer_id = self.request.http.params['customer_id']
self.invoke('customer.delete', customer_id=customer_id)
self.response.status_code = HTTPStatus.NO_CONTENT
Mount the service on a channel at /api/customers/{customer_id} and each verb reaches its handler:
# Get a customer
curl http://localhost:11223/api/customers/CUST-001
# Delete a customer
curl -X DELETE http://localhost:11223/api/customers/CUST-001
# POST is not implemented, so the channel returns 405 Method Not Allowed
curl -X POST http://localhost:11223/api/customers/CUST-001
Full CRUD resource
A resource that supports every CRUD operation implements all five handlers:
# -*- coding: utf-8 -*-
# stdlib
from http import HTTPStatus
# Zato
from zato.server.service import Service
class OrderResource(Service):
""" A full CRUD resource for orders.
"""
name = 'demo.rest.order-resource'
def handle_GET(self) -> 'None':
order_id = self.request.http.params['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) -> 'None':
data = self.request.payload
new_order = self.invoke('order.create', data=data)
new_order_id = new_order['id']
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) -> 'None':
order_id = self.request.http.params['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) -> 'None':
order_id = self.request.http.params['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) -> 'None':
order_id = self.request.http.params['order_id']
self.invoke('order.delete', order_id=order_id)
self.response.status_code = HTTPStatus.NO_CONTENT
Each verb maps to one operation:
# 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
Collection and item endpoints
A resource usually needs two endpoints - one for the collection, /api/orders, and one for individual items, /api/orders/{id}. Create two channels pointing to the same service and the service tells the requests apart by the path parameter that only the item channel carries:
# -*- coding: utf-8 -*-
# stdlib
from http import HTTPStatus
# Zato
from zato.server.service import Service
class EmployeeAPI(Service):
""" Serves both the employee collection and individual employees.
"""
name = 'demo.rest.employee-api'
def handle_GET(self) -> 'None':
# The item channel has the employee_id path parameter, the collection channel does not
if employee_id := self.request.http.params.get('employee_id'):
employee = self.invoke('employee.get', employee_id=employee_id)
self.response.payload = employee
else:
# List all employees, with the department read from the query string
department = self.request.http.GET.get('department')
employees = self.invoke('employee.list', department=department)
self.response.payload = {'employees': employees}
def handle_POST(self) -> 'None':
# POST creates a new item in the collection
data = self.request.payload
new_employee = self.invoke('employee.create', data=data)
self.response.status_code = HTTPStatus.CREATED
self.response.payload = new_employee
HEAD and OPTIONS
HEAD returns headers without a body and OPTIONS tells clients which methods the resource supports - both have their own handlers:
# -*- coding: utf-8 -*-
# stdlib
from http import HTTPStatus
# Zato
from zato.server.service import Service
class DocumentResource(Service):
""" Serves documents, with HEAD for existence checks.
"""
name = 'demo.rest.document-resource'
def handle_GET(self) -> 'None':
doc_id = self.request.http.params['doc_id']
document = self.invoke('document.get', doc_id=doc_id)
self.response.payload = document
def handle_HEAD(self) -> 'None':
# Headers only, no body - callers use it to check that a document exists
doc_id = self.request.http.params['doc_id']
exists = self.invoke('document.exists', doc_id=doc_id)
if not exists:
self.response.status_code = HTTPStatus.NOT_FOUND
def handle_OPTIONS(self) -> 'None':
# The Allow header names the methods this resource supports
self.response.headers['Allow'] = 'GET, HEAD, OPTIONS, DELETE'
For preflight OPTIONS requests that browsers send, see CORS in REST channels.
See also
| Page | What it covers |
|---|---|
| REST channels | Creating the channels that route requests to verb handlers |
| URL path matching | How methods and paths decide which channel a request reaches |
| CORS | Answering the OPTIONS preflights that browsers send |
| Error catalog | The 405 and every other status code a channel returns |