REST channels

Expose any service as a REST endpoint that clients call over HTTP.

A REST channel connects an incoming HTTP request to one of your services. When a client calls the channel's URL path, Zato invokes the service the channel names, passes the request's parameters to it and returns the service's response.

Create a channel

To create a channel, go to Connections > Channels > REST in the Dashboard, click Create a new channel and fill in the form:

  1. Name: Customer Orders
  2. URL path: the path clients call, e.g. /api/orders
  3. Service: the service that handles requests to this path
  4. Data format: JSON
  5. Click OK

The channel starts accepting requests once you click OK - configuration changes propagate to all servers automatically, with no restarts.

You can also pick a Security definition in the same form - a channel without one accepts unauthenticated requests. Basic Auth, API keys and bearer tokens are covered in authentication.

Use data models

Define the request and response as Python dataclasses and Zato parses incoming JSON into the request model and serializes the response model back to JSON. The service below creates an order from three input fields:

# -*- coding: utf-8 -*-

# stdlib
from dataclasses import dataclass

# Zato
from zato.server.service import Model, Service

@dataclass(init=False)
class CreateOrderRequest(Model):
    customer_id: str
    product_id: str
    quantity: int

@dataclass(init=False)
class CreateOrderResponse(Model):
    order_id: str
    status: str

class CreateOrder(Service):
    """ Creates an order for a customer and product.
    """
    name = 'demo.rest.create-order'

    input = CreateOrderRequest
    output = CreateOrderResponse

    def handle(self) -> 'None':

        # The parsed request, already a CreateOrderRequest instance ..
        request = self.request.input

        # .. another service creates the order ..
        order_id = self.invoke('orders.create',
            customer_id=request.customer_id,
            product_id=request.product_id,
            quantity=request.quantity
        )

        # .. and the response model serializes back to JSON.
        response = CreateOrderResponse()
        response.order_id = order_id
        response.status = 'created'

        self.response.payload = response
curl -X POST http://localhost:11223/api/orders \
  -d '{"customer_id": "CUST-001", "product_id": "PROD-123", "quantity": 2}'

Models give your IDE the field types to check before runtime, and the same definitions drive OpenAPI generation for your APIs.

Fields that the caller may omit are declared with optional from zato.common.typing_:

from zato.common.typing_ import optional

@dataclass(init=False)
class CreateOrderRequest(Model):
    customer_id: str
    product_id: str
    quantity: int
    notes: optional[str] = None

URL path parameters

Use curly braces to capture parts of the URL as parameters. For a URL path like /api/customers/{customer_id}/orders/{order_id}, Zato extracts both values and makes them available to your service through self.request.http.params:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetCustomerOrder(Service):
    name = 'demo.rest.get-customer-order'

    def handle(self) -> 'None':
        customer_id = self.request.http.params['customer_id']
        order_id = self.request.http.params['order_id']

        order = self.invoke('orders.get', customer_id=customer_id, order_id=order_id)
        self.response.payload = order
curl http://localhost:11223/api/customers/CUST-001/orders/ORD-123

params holds URL path parameters only - the query string has its own attribute, shown below.

Query string parameters

Query string parameters are available through self.request.http.GET:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# Defaults for query string parameters that the caller may omit
_default_status = 'all'
_default_limit = 100

class SearchOrders(Service):
    name = 'demo.rest.search-orders'

    def handle(self) -> 'None':
        status = self.request.http.GET.get('status', _default_status)

        # Query string values are always text
        if limit := self.request.http.GET.get('limit'):
            limit = int(limit)
        else:
            limit = _default_limit

        orders = self.invoke('orders.search', status=status, limit=limit)
        self.response.payload = orders
curl "http://localhost:11223/api/orders?status=pending&limit=10"

Other request details

The same self.request.http object holds everything else about the incoming request:

# The HTTP method, e.g. 'GET'
method = self.request.http.method

# The URL path as received, e.g. '/api/orders'
path = self.request.http.path

# All HTTP headers, with lower-case names, e.g. headers['x-api-key']
headers = self.request.http.headers

# The caller's User-Agent
user_agent = self.request.http.user_agent

JSON request body

For POST and PUT requests with JSON bodies, Zato parses the JSON automatically. Access the data through self.request.input:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class CreateCustomer(Service):
    name = 'demo.rest.create-customer'

    def handle(self) -> 'None':
        name = self.request.input['name']
        email = self.request.input['email']

        customer = self.invoke('customers.create', name=name, email=email)
        self.response.payload = customer
curl -X POST http://localhost:11223/api/customers \
  -d '{"name": "Alice Smith", "email": "alice@example.com"}'

Restrict HTTP methods

By default, a channel accepts every method from the server's [http] methods_allowed list, whose shipped default is GET, POST, DELETE, PUT, PATCH, HEAD and OPTIONS - a request with any other method receives 405 before routing. To restrict a channel further, enter one specific method in its configuration. The details are under URL path matching.

For more control, implement verb-specific handlers in your service - one service then handles GET, POST, PUT and DELETE differently on the same URL.

Data format

Set the channel's data format to match what clients send:

  • JSON - Zato parses incoming JSON and serializes responses automatically. This is the typical choice for REST APIs. See request and response handling for details and the message building examples for how responses are built through plain dot access.

  • Form data - for HTML form submissions or webhooks sending form-encoded data. Access fields through self.request.http.POST. For file uploads, see file handling.

What a channel controls

Each channel has its own settings, independent of every other channel:

  • Security: one security definition or a security group with many credentials
  • Rate limiting: per-channel rate limiting rules
  • CORS: preflights from localhost are answered automatically and the service itself serves other origins - see CORS in REST channels
  • Audit log: each user-defined channel records the requests it receives and the responses it sends in its own audit log - internal channels, such as the built-in publish/subscribe ones, are skipped
  • Response caching: a channel can serve repeated requests from its cache, without invoking the service at all - the settings open from the channel's own caching link in its Dashboard row, see response caching
  • API versioning: each major version of an API becomes its own channel and the old one is marked deprecated, with a sunset date - see API versioning
  • Delivery queue: with Use queue on in the channel's Delivery tab, each request is stored and acknowledged at once and the service runs from the queue, with retries and a dead-letter queue - see accept now, process later

Accept now, process later

Turn on Use queue in a channel's Delivery tab and the channel stores each request as it arrives, answers HTTP 200 with {"is_ok": true, "cid": "..."} and runs the service later, from the queue - the service receives the request exactly as it would have had it run at once, and a run that raises is retried under the channel's retry and dead-letter settings. The service can customize the acknowledgment by overriding get_queue_response, which runs as the request is accepted and reads the stored message's id from self.request.queue.msg_id.

The details, the settings and the enmasse key for a fixed acknowledgment are in delivery retries and dead-letter queues.

See also

PageWhat it covers
URL path matchingHow a request finds its channel - paths, methods and priorities
AuthenticationBasic Auth, API keys and bearer tokens for channels
HTTP verbsOne service responding differently to GET, POST, PUT, PATCH and DELETE
Enmasse referenceEvery channel_rest key for YAML-based deployments
Delivery retries and dead-letter queuesThe queue a channel accepts requests into and the service's run from it

Learn more