Diving deep into REST API channels

We begin in 2021 with a deep dive into Zato REST API channels. What are they? How to use them efficiently? How can they configured for maximum flexibility? Read on to learn all the details.

A sample service

First, let's have a look at a sample service that we want to make available to API clients.

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

# Zato
from zato.server.service import Service

class GetUserDetails(Service):
    """ Returns details of a selected user.
    """
    name = 'api.user.get-details'

    class SimpleIO:
        input_required = 'user_name'
        output_required = 'email', 'user_type'

    def handle(self):

        # Log what we are about to do.
        self.logger.info('Returning details of `%s`', self.request.input.user_name)

        # In real code, we would look up the details in a database,
        # but not necessarily in a cache - read the article why it is not needed.
        details = {
            'email': 'my.user@example.com',
            'user_type': 'ABC'
        }

        # Return the response now.
        self.response.payload = details

The first thing that may strike you is that the code is on a very high level - it just has access to a user_name and some data is returned but there is no mention of REST, no data serialization, caching or anything that is not the business functionality of returning user details.

This is by design. In Zato, services focus on what they actually need to do and the lower-level details are left to the platform. In other words, the service does not need to be concerned with peculiarities of a given transport method, it just has its input to process and output to produce. It is channels in front of a service that deal with all such aspects and the service concentrates on higher level logic.

This makes it possible to employ the same service in other contexts - for instance, we are describing REST channels today but the very same service could be invoked from the scheduler, through AMQP, IBM MQ or via other channels, including multiple REST ones, helping you in this way design a reusable Service-Oriented Architecture (SOA).

With that in mind, we can advance to REST channels now.

REST API channels

In Zato web-admin, a definition of a sample REST channel making use of our service may look like below.

Now that we have created a new channel, we can invoke it to confirm that it works as expected.

% curl localhost:17010/api/v1/user/my.username ; echo
{"email": "my.user@example.com", "user_type": "ABC"}
%

In server logs:

INFO - api.user.get-details - Returning details of `my.username`

Everything is fine and we can proceed.

Just one note about channels, remember that there can be many channels pointing to the same service - this lets you reuse the services in various scenarios, e.g. you can have a separate REST channel with its own security definition for each external application connecting to your APIs.

Whenever you add, modify or delete a channel, the service as such is unchanged, nor are any other channels. Conversely, when you update a service, for instance adding new functionality, all channels using it will automatically invoke the new version of the service.

Channels come with good defaults when you create them but it is always possible to customize them to one's particular needs. Let's go step by step through each attribute of a channel's definition.

Basic information

  • Name - Each channel has a unique name which can be arbitrary, depending on your naming conventions.

  • Active - An inactive channel cannot be invoked; doing so will yield a 404 error.

URL path & query string

  • URL path - URL paths need to be unique. Each can contain one or more patterns to match input parameters - this is why in our service we were able to reference self.request.input.user_name - it was extracted from the URL path.

  • Match slash - Sometimes, URL path parameters sent from API clients will contain the slash character which normally is used to separate path components. This checkbox controls whether path patterns should match a slash or not.

  • URL params - QS over path vs. Path over QS. Usually, applications will send parameters in one place only, e.g. only in the URL path or in query string parameters. But what if an application has good reasons to send parameters in both the query string and URL path, for instance /api/v1/user/my.username?user_name=my.other.username - this happens from time to time and this setting lets one control which of the two will take precedence.

  • Merge to request - Used if parameters are sent via query string in addition to the URL path. If checked, such query string parameters will be accessible through self.request.input, otherwise, they will exist only in self.request.http.GET.

  • Params priority - This is similar to the options above but it will control the behavior if parameters are sent in both JSON message body as well as in URL path or query string - it lets one decide which will have higher priority and which will become available in self.request.input.

HTTP metadata & data format

  • Method - it is possible to specify that only specific HTTP method can be used to invoke the channel. This is not always needed. For instance, the service above can work just fine with either GET or POST, depending on what the caller prefers. Conceptually, this is a GET request but some API clients will always use POST so this is why filling out this field is not always needed or advised.

A service can handle multiple methods either as a whole and a channel can dispatch requests to specific parts of the service depending on which method is used, i.e. you can add handle_GET, handle_POST and other such methods independently, resulting, for instance, in a User service that handles CRUD via respective handle_* methods.

  • Encoding - Can be set to "gzip" to enable compression of responses from the channel.

  • Data format - Can be one of JSON, XML or HL7 to enable auto-de/serialization of requests and responses. Note that the original request, prior to any processing, is always available in self.request.raw_request.

  • Accept header - To what Accept HTTP headers the channel should react. Note that, just like with methods, it is possible for the service to handle each header separately, which lets one have the same service produce different responses depending on what the client's Accept header dictates.

API services & security

  • Service - The service that is mounted on this channel and which will be given incoming requests on input, as in the code example above. The same service may be mounted on multiple channels.

  • Security definition - What kind of an authentication mechanism this channel requires. Can be one of Basic Auth, API key, JWT, Vault, WS-Security or XPath. Each channel may have a different security definition assigned and the same definition can be assigned to multiple channels. Note that, to avoid any misunderstandings, you need to explicitly choose "No security definition" if the channel should not handle authentication itself, perhaps because the service should do it on its own, i.e. it is not possible to forget to choose something here and leave a channel unsecured by mistake.

  • RBAC - A channel can also use Role-Based Access Control definitions where each API client is assigned fine-grained permissions to individual methods and roles, possibly hierarchical one, which control if a particular client can invoke the channel's service. For instance, some API clients may be allowed to only read (GET) data whereas different ones will be able to create, update and delete it too (POST, PATCH and DELETE).

API caching

  • Cache type - Optionally, can be set to either built-in or Memcached. Requests matching previous ones will be automatically served from a specific cache assigned to the channel. With the built-in cache, the response is served from RAM directly, resulting in very fast responses. It is possible to browse, modify or delete the cached data, as below:

API rate limiting

The rate limiting stanza was not expanded previously so let's do it now to add a rate limiting definition to our channel. As usual, the limits can be set separately for each channel.

In this particular case, clients connecting from the internal network will be allowed to issue 30 request per minute but connections from localhost can invoke it 50k times an hour. And everyone else is limited to 100 requests a day.

Message log

Just like with rate limiting, these options were not expanded in the initial screenshot so here they are. Each channel can keep N last messages received, sent or both. It is also possible to say that only the first X kilobytes of a given message are to be stored - some messages may be too large for it to be practical to keep them in their entirety. When the log becomes full, the oldest messages are discarded, making room for newer ones.

Stored messages can be browsed in web-admin, immediately letting one know what a given channel receives and sends.

Such a message log is a feature common to other elements of Zato, e.g. HL7 or WebSocket connections have their own message logs too.

Documentation and OpenAPI

It is good that we have a REST channel but how do we let others know about it? On the one hand, you can simply notify your partners and clients through documentation, wiki pages or other non-automated means.

On the other hand, you can also supply auto-generated API documentation and OpenAPI definitions, like below. This means that API clients matching your services can be generated automatically.

And by the way, if you need a WSDL for SOAP clients, the generated documentation includes it too.

Invoking REST APIs

This is everything about REST channels today but one question may be still open. If channels are means for handling incoming connections then how does one make requests to REST API endpoints external to Zato? How to invoke other other people's microservices?

The answer is that this is what Zato outgoing connections are for and we will cover REST outgoing connections in a future post in a way similar to how we went through REST channels in this one.