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

PageWhat it covers
REST channelsCreating the channels that route requests to verb handlers
URL path matchingHow methods and paths decide which channel a request reaches
CORSAnswering the OPTIONS preflights that browsers send
Error catalogThe 405 and every other status code a channel returns

Learn more