Schedule a demo

Where to keep API configuration

Zato lets you keep your own configuration in .ini files that your services can access instantly from memory. This is where you store API endpoints, mapping tables, business rules, and any other values that shouldn't be hardcoded.

Configuration files go in the config/user-conf directory of your project on the host machine. When you deploy with Docker, this directory is mounted into the container, so Zato can read the files at startup. Each file becomes accessible through self.config in your services, using the file name (without extension) as the first key.

For instance, if you create a file called airports.ini, in your service it will be is accessible as self.config.airports, and sections will additional keys, so [api] with timeout = 30 becomes self.config.airports.api.timeout.

Creating a configuration file

On your host machine, create an .ini file in your project's config/user-conf directory. For example, config/user-conf/airports.ini:

[api]
base_url = https://api.airport-ops.example.com
timeout = 30
max_retries = 3

[security]
token_sec_def_name = Airport.Ops.Token

[terminals]
domestic = T1
international = T2
cargo = T3

[[gate_prefixes]]
T1 = A
T2 = B
T3 = C

The file uses a simple structure:

  • Sections are defined with single brackets [section_name]
  • Nested sections use additional brackets - double [[subsection]], triple [[[deeper]]], and so on for arbitrary nesting depth
  • Key-value pairs are written as key = value
  • Comments start with #

Accessing configuration in services

In your service, access the configuration through self.config using dot notation. The first key is the file name (without .ini), followed by section names and keys:

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

# Zato
from zato.server.service import Service

class GetFlightInfo(Service):

    def handle(self):

        # Access simple values
        base_url = self.config.airports.api.base_url
        timeout = self.config.airports.api.timeout

        # Access nested sections
        t1_prefix = self.config.airports.terminals.gate_prefixes.T1

        self.logger.info('Using API at %s with timeout %s', base_url, timeout)
        self.logger.info('Terminal 1 gate prefix: %s', t1_prefix)

You can nest sections as deep as you need. Each level of nesting in the .ini file becomes another level of dot access in your code.

Using sections as dictionaries

Each configuration section also behaves like a Python dictionary. You can iterate over keys, look up values with square brackets, check for key existence, and use all the standard dict methods:

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

# Zato
from zato.server.service import Service

class ListTerminals(Service):

    def handle(self):

        terminals = self.config.airports.terminals

        # Iterate over all keys
        for terminal_name in terminals:
            terminal_code = terminals[terminal_name]
            self.logger.info('Terminal %s -> %s', terminal_name, terminal_code)

        # Check if a key exists
        if 'cargo' in terminals:
            cargo_code = terminals['cargo']
            self.logger.info('Cargo terminal: %s', cargo_code)

        # Get with default
        vip_terminal = terminals.get('vip', 'T1')

        # Get all keys
        all_terminal_names = list(terminals.keys())

        # Get all values
        all_terminal_codes = list(terminals.values())

        # Get all items
        for name, code in terminals.items():
            self.logger.info('%s = %s', name, code)

This means you can use configuration sections both for direct access when you know the key names, and for dynamic lookups when you need to iterate or search.

Value types

Zato automatically converts configuration values to appropriate Python types.

For instance, let's say you have this config:

[api]
base_url = https://api.airport-ops.example.com
timeout = 30
max_retries = 3
debug_mode = False

[terminals]
active_gates = A1, A2, A3, B1, B2
gate_capacity = {'A1': 200, 'A2': 150, 'B1': 300}

Here's how it would be converted:

  • Integers - timeout = 30 becomes Python int 30
  • Booleans - debug_mode = False becomes Python bool False
  • Lists - active_gates = A1, A2, A3, B1, B2 becomes Python list ['A1', 'A2', 'A3', 'B1', 'B2']
  • Dicts - gate_capacity = {'A1': 200, 'A2': 150, 'B1': 300} becomes Python dict
  • Strings - base_url = https://api.airport-ops.example.com remains a Python str

Nested sections

Nested sections become nested objects. Each level of brackets adds another level of dot access, so you can organize complex configuration hierarchically and access it naturally in code.

[terminal_t1]

[[gates]]

[[[gate_a1]]]
capacity = 200
airlines = FI, AF

[[[gate_a2]]]
capacity = 150
airlines = BA

In your service:

  • self.config.airports.terminal_t1 - the [terminal_t1] section
  • self.config.airports.terminal_t1.gates - the [[gates]] subsection
  • self.config.airports.terminal_t1.gates.gate_a1 - the [[[gate_a1]]] subsection
  • self.config.airports.terminal_t1.gates.gate_a1.capacity - the value 200 (as int)
  • self.config.airports.terminal_t1.gates.gate_a1.airlines - the list ['FI', 'AF']

Mapping tables

A common pattern is storing mapping tables - translating codes from external systems to your internal values:

[mappings]

[[aircraft_types]]
738 = Boeing 737-800
763 = Boeing 767-300
388 = Airbus A380-800
320 = Airbus A320
321 = Airbus A321

[[delay_reasons]]
WX = Weather
TC = Technical
OP = Operational
SC = Security
CR = Crew

[[airline_codes]]
FI = Icelandair
AF = Air France
BA = British Airways

Use these mappings in your service:

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

# Zato
from zato.server.service import Service

class TranslateFlightData(Service):

    def handle(self):

        # Get the mapping tables
        aircraft_types = self.config.airports.mappings.aircraft_types
        delay_reasons = self.config.airports.mappings.delay_reasons

        # Translate codes
        aircraft_code = '738'
        aircraft_name = aircraft_types[aircraft_code]

        delay_code = 'WX'
        delay_description = delay_reasons[delay_code]

        self.logger.info('Aircraft: %s', aircraft_name)
        self.logger.info('Delay reason: %s', delay_description)

Environment variables for secrets

For sensitive values like API keys, passwords, and tokens, don't put them directly in the config file. Instead, reference environment variables using the $ prefix:

[credentials]
api_key = $Airport_API_Key
client_secret = $Airport_Client_Secret

[database]
password = $Airport_DB_Password

When Zato loads the configuration, it automatically replaces $Variable_Name with the value from the corresponding environment variable. If the variable isn't set, a warning is logged.

In your service, you access these values exactly the same way as any other config value:

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

# Zato
from zato.server.service import Service

class ConnectToAirportAPI(Service):

    def handle(self):

        # These come from environment variables, but you access them the same way
        api_key = self.config.airports.credentials.api_key
        client_secret = self.config.airports.credentials.client_secret

        # Use them in your API calls
        self.logger.info('Connecting with API key: %s...', api_key[:4])

DevOps and container deployments

When deploying with Docker, you pass environment variables to the container and mount your configuration directory. The DevOps deployment tutorial covers this in detail.

The typical setup:

  1. Mount the config directory from your host to /opt/hot-deploy/yourproject/config/user-conf inside the container
  2. Pass environment variables either directly with -e flags or through an env.ini file mounted at /opt/hot-deploy/enmasse/env.ini

Example from a deployment script:

docker run \
    --mount type=bind,source=$host_root_dir/config/user-conf,target=$target/myproject/config/user-conf \
    --mount type=bind,source=$host_root_dir/config/auto-generated/env.ini,target=$target/enmasse/env.ini \
    $package_address

The env.ini file contains the environment variables that your config files reference:

[env]
Airport_API_Key=your-api-key-here
Airport_Client_Secret=your-secret-here
Airport_DB_Password=your-password-here

This separation means your configuration structure is in version control, but the actual secrets are injected at deployment time from your secrets management system.

Real-world example

Here's a more complete example showing how configuration fits into a real service that integrates with an airport operations API:

[api]
base_url = https://api.airport-ops.example.com
timeout = 30
token_sec_def_name = Airport.Ops.Token

[defaults]
days_to_check = 7
max_results = 100

[terminals]
domestic = T1
international = T2

[[gate_ranges]]
T1 = A1-A20
T2 = B1-B40

[credentials]
api_key = $Airport_API_Key
# -*- coding: utf-8 -*-

# stdlib
from dataclasses import dataclass

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

@dataclass(init=False)
class FlightSearchRequest(Model):
    terminal: str
    date: str

@dataclass(init=False)
class Flight(Model):
    flight_id: str
    gate: str
    status: str

class SearchFlights(Service):

    input = FlightSearchRequest

    def handle(self):
        request:'FlightSearchRequest' = self.request.input

        # Get configuration
        config = self.config.airports

        # Build API request
        base_url = config.api.base_url
        timeout = int(config.api.timeout)
        max_results = int(config.defaults.max_results)

        # Translate terminal name to code
        terminal_code = config.terminals[request.terminal]

        # Get gate range for this terminal
        gate_range = config.terminals.gate_ranges[terminal_code]

        self.logger.info('Searching flights in terminal %s (gates %s)',
            terminal_code, gate_range)

        # Make the API call
        conn = self.out.rest['Airport Ops API'].conn
        api_response = conn.get(self.cid, params={
            'terminal': terminal_code,
            'date': request.date,
            'limit': max_results
        })
        flights_data = api_response.data

        # Process results
        flights = []
        for item in flights_data:
            flight = Flight()
            flight.flight_id = item['FlightID']
            flight.gate = item['Gate']
            flight.status = item['Status']
            flights.append(flight)

        self.response.payload = {'flights': flights}

Learn more