Skip to main content

API Clients

Given that modules should not have direct dependencies on other modules and given that we're building a micro / macro service based architecture we need a mechanism to communicate between modules when the need arises.

There are two approaches we use in general, depending on whether you are reading or writing data.

Writing Data (Event Sourcing)

If you need to write data to an internal service this should generally be done via events using the eventually consistent paradigm rather that via API calls which can be hard to orchestrate when failures occur.

For more information on how we raise and handle events please the events documentation here

Reading Data (API Clients)

If you need to read data from a service but not change it you should use the API clients to make API calls to retrieve the data you need. To facilitate this we are auto generating API clients that use axois to trigger API calls between our services

API Clients are components that live in a separate repository hectare-Agritech/platform-lib there is a GtHb workflow in this repo which is triggered on successful completion of the CI workflow in the platform repo.

The workflow in lib repo will pull the latest bit components from the cloud and regenerate clients and events components which can be referenced by the front end and other services within the back end.

Any module that needs to communicate with other modules should reference the modules client rather than the module itself, for example

import { SystemService } from '@hectare/lib.clients.system'

Clients and event components are prefixed with lib

We are also generating the types exposed by each modules API so we have a strongly typed client for each module.

Each auto generated API client exposes a set of classes which expose the methods to trigger each path defined in the OpenAPI schema. There is an ApiClientFactory class (components/common/clients/api-client-factory.ts) which manages creating service instances and configuring them.

Use a client as follows

// import InventoryService and its OpenAPI config from the client component
import { RecordDetail, InventoryService } from '@hectare/lib.clients.inventory'

// Get an instance of the InventoryService using the factory on Context
const client = context.client(InventoryService)

// Call the recordGetByIds() operation
const records = await client.recordGetByIds(payload.map(p => p.recordId))

Test API Client

We use the TestApiClient to trigger API handlers without needing an HTTP service running, this allows us to make inter-service API calls from integration tests, without this these tests would always fail as the APIs will not be running on the CI server for example.

see tests/utils/api-client-factory.ts for more details

Authentication

We need a mechanism to authenticate API calls between services, the intention currently is to keep things simple and access internal services via the internet rather than via the AWS VPC.

Therefore each API call will need to be authenticated with a valid token.

The ApiClientFactory takes care of authenticating the API call using the currently logged in tokens, we simply copy across the auth tokens to the headers of the APi request

components/common/clients/api-client-factory.ts