Project structure and naming conventions
The blueprint layout, directory prefixes and service naming that keep a project predictable.
A well-structured project is one where anyone can predict where a piece of code lives before opening a single file. This page describes the conventions that Zato projects follow - the directory layout, what belongs where, and how services are named.
The layout
The starting point is the project blueprint, the same structure the deployment tutorial works with:
myproject
├── config
│ ├── enmasse
│ │ └── enmasse.yaml # Channels, connections, security - the environment as YAML
│ ├── python-reqs
│ │ └── requirements.txt # Extra Python packages your services need
│ └── user-conf
│ └── myconf.ini # Your own configuration values
├── impl
│ ├── scripts
│ │ └── run-container.sh # Starts the environment
│ └── src
│ ├── api
│ │ ├── billing.py
│ │ └── employee.py
│ ├── common
│ │ └── models.py
│ └── util
│ └── formatting.py
└── testing
└── tests
└── test_billing.py
Configuration and code are kept apart - config holds everything declarative, impl/src holds everything programmable, and testing holds the tests. The whole tree lives in git and is the single source of truth for the environment, per the GitOps model that deployment describes.
What belongs in each directory
Directories under src start with one of eight prefixes, and the prefix states the role of the code inside:
| Prefix | What lives there |
|---|---|
api | Services that external callers invoke - the entry points of your integrations |
services | Same role as api - use whichever name reads better in your project |
channel | Services bound to specific channel types, e.g. MLLP or WebSocket handlers |
adapter | Services that talk to one external system each, wrapping its API details |
core | Business logic that is independent of any transport or external system |
model | Data models - the dataclasses that define inputs and outputs |
common | Code shared by everything else - constants, base classes, shared models |
util | Helper functions with no business meaning - formatting, parsing, conversions |
The prefixes are not just documentation - hot deployment deploys them in dependency order, shared code first, so common, util and model are always in place before the api services that import them. A directory only needs to start with the prefix, so api_billing and api_hr both count as api.
The channel, adapter and core layers
The three middle prefixes express one pattern - an integration is a chain of three responsibilities, each in its own layer:
- A channel service receives the request and knows about the transport - HTTP headers, HL7 wrapping, file encodings - and nothing about business rules
- A core service makes the business decisions and knows nothing about transports or external systems
- An adapter service calls one external system and knows its API's details - its endpoints, its error codes, its pagination - and nothing about why it is being called
The payoff is that each layer changes independently - a new transport is a new channel service in front of the same core, and swapping a CRM vendor replaces one adapter without touching the business logic. The REST adapter pattern is the ready-made building block for the adapter layer.
For a small project, the layers can be a single api directory with three modules. The point is the separation of concerns, not the number of directories.
Naming services
Service names are dotted, lowercase, with words separated by dashes:
# -*- coding: utf-8 -*-
from zato.server.service import Service
class GetCustomerDetails(Service):
name = 'crm.customer.get-details'
def handle(self):
...
The convention is <project or system>.<entity>.<action>:
crm.customer.get-detailsbilling.invoice.createhr.employee.sync-all
Names shape everything downstream - they appear in logs, in the Dashboard, in REST channels and in self.invoke calls, so a consistent scheme makes an environment self-describing. Group by system or business area first, never by transport - crm.customer.get-details stays correct when the same service gains a second channel.
Class names mirror the last part of the service name - GetCustomerDetails for get-details - and one module holds the services of one entity or one narrow area, so api/customer.py is where every crm.customer.* service lives.
Naming everything else
The same spirit extends to the objects defined in enmasse and the Dashboard:
- REST channels are named after what they expose, e.g.
api.customerwith the URL path/api/customer/{customer_id} - Outgoing connections are named after the system they reach, e.g.
CRMorBilling API, because that is the name your code uses -self.rest['Billing API'] - Security definitions are named after who uses them, e.g.
api.customer.basic-authorPartner X API Key