API
Best practices
HTTP
Python
CallerAPI: An Innovative Microservice Processing Solution
Feb 18, 2025
15 min read

Microservice communication doesn’t have to be complex. In this article, written by our developer Eduard, we introduce CallerAPI—a powerful internal tool designed to streamline interaction between microservices. Built to support over 40 services in our document processing platform, CallerAPI reduces boilerplate code, simplifies error handling, and supports environment configuration with ease. Discover how Eduard’s solution improves productivity and makes service integration effortless.
Disclaimer
CallerAPI was developed exclusively for our company’s internal needs and is intended to simplify the interaction between microservices within our document processing platform. The main goal of developing this package was to create a convenient and effective tool that would simplify the integration and interaction of a large number of microservices, allowing our developers to focus on solving business problems, rather than on the routine work of setting up and calling services.
I would like to share an experience and best practices, as they may be useful to other developers facing similar problems.
Introduction
In the modern world, microservice architecture has become one of the standards for developing complex software solutions. It allows you to split your application into many small, independent services, making them easier to develop, test, and deploy. However, with the increase in the number of microservices, problems arise between their interaction and coordination. In the company, I was faced with the need to integrate more than 40 microservices and the developed CallerAPI package greatly simplified this process.
Problem Overview
With a large number of microservices, it becomes inconvenient and difficult to manage their interactions. Each service has its own API, and calling one service from another requires writing a lot of code to configure HTTP requests, error handling, etc. This leads to code duplication, increased development and testing complexity.
Below I listed some related problems of microservices interaction:
- Difficulty of multiple microservices integration.
- Necessity of writing a big code volume for each HTTP-request.
- Increased complexity of error handling and session management.
- Difficulties in maintaining and updating the code.
Solution
To solve these problems, the CallerAPI package was developed, which allows you to easily and conveniently interact with microservices through a unified interface. The main idea of CallerAPI is to collect all services and their APIs in one package that can be installed in any microservice and used with minimal effort.
The main advantages of CallerAPI
- Simplification of calling microservices by using a single interface.
- Reduction of code volume by unifying interaction methods.
- Convenient error handling and session management.
- Easy code maintenance and update.
- Convenient configuration of environment variables (env).
Realization
The main components of CallerAPI
1.1. AIOHttpClient
The AIOHttpClient
class is responsible for managing HTTP client sessions. It provides creation and configuration of aiohttp.ClientSession
for each service.
class AIOHttpClient:
"""
A class for managing AIOHttp client sessions.
"""
def __init__(self, base_url: str) -> None:
self.base_url = base_url
def aiohttp_client(
self, service_id: Union[ServiceID, str], timeout: Optional[Union[int, aiohttp.ClientTimeout]] = None
) -> aiohttp.ClientSession:
"""
Return aiohttp.ClientSession based on the given service ID and timeout.
Creates a new session if it does not exist or the previous one is closed.
"""
import logging
logging.basicConfig(level=logging.CRITICAL)
if not timeout:
timeout_settings = aiohttp.ClientTimeout(total=300, sock_read=180)
elif isinstance(timeout, int):
timeout_settings = aiohttp.ClientTimeout(total=timeout, sock_read=180)
elif isinstance(timeout, aiohttp.ClientTimeout):
timeout_settings = timeout
headers = {
"Accept": "application/json",
}
client = aiohttp.ClientSession(
headers=headers,
base_url=self.base_url.format(use_service_id=service_id.value.lower()),
timeout=timeout_settings,
)
return client
It also allows to flexibly configure timeouts for HTTP sessions:
def aiohttp_client(self, service_id: Union[ServiceID, str], timeout: Optional[Union[int, aiohttp.ClientTimeout]] = None) -> aiohttp.ClientSession:
...
1.2. BaseApi
The BaseApi
class is the base class for all API classes. It combines all the methods for executing HTTP requests (GET, POST, DELETE, etc.), which allows you to minimize code repetition and centralize request processing.
class BaseApi:
"""
A base class for making API calls
"""
http_client = None
def __init__(
self,
app_id: str,
base_url: str,
timeout: Optional[Union[int, aiohttp.ClientTimeout]] = None,
) -> None:
self.app_id = app_id
self.http_client = AIOHttpClient(base_url=base_url)
self.timeout = timeout
async def _get_client(self) -> aiohttp.ClientSession:
"""
Return ClientSession instance.
"""
return self.http_client.aiohttp_client(self.app_id, self.timeout)
async def _request(self, method: str, ep: str, data: Any = None, encode: bool = True) -> aiohttp.ClientResponse:
"""
Make an HTTP request using the given method, endpoint, and data.
"""
http_client = await self._get_client()
json_compatible_data = jsonable_encoder(data) if encode else data
if method == "post":
response = await http_client.post(ep, json=json_compatible_data)
elif method == "get":
response = await http_client.get(ep, params=data)
elif method == "delete":
response = await http_client.delete(ep, params=json_compatible_data)
elif method == "patch":
response = await http_client.patch(ep, json=json_compatible_data)
elif method == "put":
response = await http_client.put(ep, json=json_compatible_data)
elif method == "delete_body":
response = await http_client.delete(ep, json=json_compatible_data)
elif method == "post_data":
response = await http_client.post(ep, data=json_compatible_data)
else:
raise ValueError(f"Unsupported method: {method}")
return response
Please note that there are 2 additional methods
post_data
anddelete_body
used here.
Environment Configuration Example
from starlette.config import Config
config = Config(".env")
DEPLOY = config("DEPLOY", default=False)
BASE_URLS = {
"SERVICE_NAME": "http://localhost:6001",
...
}
def get_config(key: str) -> str:
if DEPLOY:
DEFAULT_URL = config("DEFAULT_URL", default="http://localhost:6000")
return config(key, default=DEFAULT_URL)
else:
return config(key, default=BASE_URLS[key])
for key in BASE_URLS:
globals()[key] = get_config(key)
Usage Examples
ServiceID
class using Enum:
class ServiceID(str, Enum):
foo = "Foo"
...
6.1. Example class for calling a service
class Foo(CommonEndpoints):
"""
A class for calling Foo service.
"""
def __init__(self, timeout: Optional[Union[int, aiohttp.ClientTimeout]] = None) -> None:
super().__init__(
app_id=ServiceID.foo,
base_url=S.FOO_URL,
timeout=timeout,
)
async def v1_process(self, data: model.FooInput) -> aiohttp.ClientResponse:
"""
Make a post request to the /v1/process endpoint.
"""
return await self._request("post", "/v1/process", data)
6.2. File transfer and multipart/form-data usage
class Bar(CommonEndpoints):
"""
A class for calling Bar service.
"""
def __init__(self, timeout: Optional[Union[int, aiohttp.ClientTimeout]] = None) -> None:
super().__init__(
ServiceID.bar,
S.Bar_URL,
timeout,
)
async def v1_file(self, subdomain: str, client_id: str, file: UploadFile) -> aiohttp.ClientResponse:
"""
Make a post request to the /v1/file endpoint.
"""
ep = f"/v1/file?subdomain={subdomain}&client_id={client_id}"
form = aiohttp.FormData()
form.add_field("file", await file.read(), filename=file.filename, content_type="application/octet-stream")
return await self._request("post_data", ep, form, False)
Conclusion
CallerAPI greatly simplifies communication between microservices by providing a unified interface for calling APIs. It reduces the amount of code required to integrate services, improves session management and error handling, which ultimately improves productivity and ease of development. Additionally, CallerAPI provides convenient mechanisms for configuring environment variables, as well as support for file transfer and multipart/form-data.