Skip to main content

Code

This document outlines the key concepts we adhere to when organising and writing code as well as a few miscellaneous but important points to bear in mind when working on the platform.

You will also find steps necessary to add new APIs or Lambdas ensuring the approach adheres to our coding practices

Naming Conventions


Please apply the following naming conventions to maintain consistency across the codebase.

Filenames: snake-case.ts\ Types & Classes: PascalCase\ OpenAPI Schemas: PascalCase\ Interfaces: IPascalCase (prefixed with I)\ Variables & Function names: camelCase\ APIs: should be plural, i.e. POST /api/inventory/records, GET /api/inventory/records/1-A\ API & Event Handlers should be named after the domain entity they are related to, for example recordCreate, storagePatch

As another rule of thumb a lot of thought has gone into the file / folder structure of the codebase, this means naming things especially files are usually contextual based on their location within the codebase, for example

modules/inventory/handlers/api/records/create.ts\ modules/inventory/handlers/api/storage/create.ts

The files are named the same but are differentiated by their path, again this maintains a consistent and clean structure to the codebase.

Code Imports

All of our code is referenced using typescript paths, this allows us to import code using semantic naming rather than complex hard to maintain paths

import { Context } from '@hectare/platform.components.context'

not

import { Context } from '../../components/context'

Code Organisation

The vast majority of the code in the platform is triggered via an API call or a Lambda invocation, the entry points for both are handlers which have a specific method signature depending on whether its an API call or Lambda event

export type ApiHandler = (context: Context) => Promise<Response>\ export type EventHandler = (context: Context) => Promise<void>

In general for simple handlers we put all the relevant code into the handler itself, for more complex processes we would want to extract the code from the handler and organise it in the relevant folder in each module using the same folder naming conventions we use in the handlers path and broken down into logical chunks of testable code.

Sometimes code needs to be shared between events and APIs, if this is the case the code should be stored in the modules/[module]/core/code folder so its accessible for both the API and events components.

Code that is only used in APIs or Events should be kept in the relevant API or event component.

Unit tests should be written in a file matching the file name containing the code being tested with .spec.ts suffix, for example

modules/inventory/handlers/api/records/create.ts\ modules/inventory/handlers/api/records/create.spec.ts

Components, Modules & Services

We take a component-driven development approach, meaning we develop our software by building loosely-coupled independent components. At every layer we're composing smaller components into larger ones, i.e. services are composed of modules, modules are composed of components and components are composed of functions.

Components

Components are the smallest unit of code encapsulation in the repository. Components can have dependencies on other components but not modules or services.

Modules

Modules are units of business functionality centered around a set of related domain entities (i.e. Inventory, Customers, Geo). Generally a domain area would have 3 modules, core, api and events. Core should contain any code that needs to be shared between events and API such as models, indexes and any common code. API modules contains all API related code including handlers, OpenAPI definitions etc. Event modules contain code for any background events that a particular domain needs to handle.

Modules can have dependencies on other modules within a related domain (i.e. inventory.core & inventory.api) but we should avoid having dependenmcies on modules from another domain as the liklihood is these modules will be deployed to a different service and communication should therefore be over http not in-process.

Each API module exports a Manifest file which specifies

Services

Services are lightweight units of deployment composed of one or more modules, they contain no domain code themselves. We maintain a private database per service / domain which should bne inaccessible to other services.

We have a service per domain we deal with (Inventory, Customers, Geo etc). The service folders (/services/inventory /services/customers etc) are comprised of an api folder and an lambda folder. These folders contain configuration and cloudformation code to specify how the service resources should be deployed.

API composition is handled via the service API entry point (services/inventory/api/index.js). This file imports one or more manifests from the modules it wants to expose and configures itself using these manifests. This gives us composibility for out services and allows us for example to easily run all services under one service in local but split them out i nto a service per domain when deploying to AWS

APIs are deployed to AWS Fargate using a docker container.

A service also specifies the configuration for any background events deployed to Lambda. The lambda folder contains a handlers directory under which is a directory per lambda we deploy, the entry points in these folders should be very lightweight, they are intended to route through to the handler defined in the related events module.

We build and tree-shake our lambda code using the esbuild.js script in each lambda folder, this ensures we only package the code we need.

New lambdas need to be added to the template.yml file to ensure they are created / updated when we deploy the service events to AWS.

Finally the api folder under services also contains data migration scripts for the service database, these are JavaScript files which are executed as part of the deployment to allow us to modify the database during a release, see RavenDB for more info on data migrations

Error Handling

We generally do not create custom error types for different errors we throw within the platform, we have a general purpose error in common component called HectareError. HectareError can be passed the incoming Event to pull off relevant information to assist with logging and also exposed a code field which can be used to indicate the type of error.

The sentry middleware component components/context/middleware/sentry.ts handles errors that occur during the execution of a handler and sets up the appropriate response.

In production the stack trace is hidden, all other environments its returned in the API response.