CAITSGet started

How CAITS works

A CAITS app has two sides, the backend and the frontend. Each side holds contexts, and each context has four layers. This page shows how a message moves through the layers, on each side, and what keeps each layer in its place.

Sides, contexts and layers

  • Two sides. The backend answers HTTP requests, in Node.js. The frontend runs the pages, on the server and then in the browser. They never import each other: they talk only through the HTTP API.
  • Contexts. Each side holds contexts: one folder for each part of the business, such as messages. A context never imports another one, except the backend context shared. See Add a context.
  • Four layers. Each context has the same four layers. The dependencies point to the domain.
LayerWhat it holdsIt may import
DomainThe business: entities, value objects, errors, events, and the portsNothing but itself, and three small types of CAITS: Result, Token, DomainEvent
ApplicationThe use cases: one folder each, with its DTOs, its handler, and its testThe domain
InfraThe adapters of the ports: a file, a database, an HTTP APIThe domain (and the application, in the frontend)
PresentationThe controllers in the backend; the store and the views in the frontendThe application and the domain

The routes of src/routes/ are not a layer: a page places views, and a handler of the API gives the request to a controller.

The frontend

An action of the user goes around a loop: the view never calls a handler, and a handler never changes the HTML. Events and the store link them.

PRESENTATIONAPPLICATIONDOMAININFRAEventDTOHTTPEventstateUserViewEventBusStoreHandlerMessageBusEntitiesGatewayServer API123456678
  1. The user acts on a view: they submit the form of a new message.
  2. The view publishes an event on the event bus: MessageAdditionRequested.
  3. The store gets the event. Its send turns it into a DTO, AddMessageCommand, and its reduce changes the state: a change is on its way.
  4. The store sends the DTO on the message bus.
  5. The message bus gives the DTO to its handler, AddMessageHandler.
  6. The handler calls its port, MessagesGateway. The adapter of the port, HttpMessagesGateway, posts the text to the API of the backend.
  7. The handler publishes the answer as an event: MessagesChanged, or MessagesChangeFailed with the reason.
  8. The store reduces the event into the new state, and the view, which follows the store, shows it. After a change, the store also asks for the list again.

src/frontend/messages/presentation/view/message-board/MessageBoard.marko

<form class="message-board__composer" onSubmit(event) {
  event.preventDefault();
  focusAfter = { done: "new-message", failed: "new-message" };   // where the focus goes when the answer comes
  frontendOf($global, "messages").events.publish(new MessageAdditionRequested(text));
}>

On the server, the first HTML of a page already holds the data: the loader of the view (loadMessageBoard.ts) asks the store for the messages, and waits for the answer, before the page is sent. In the browser, the same view then follows the store.

The backend

A request goes through the layers and back, in one turn:

PRESENTATIONAPPLICATIONDOMAININFRARequestInput DTOOutput DTOResponseClientControllerMessageBusHandlerEntitiesRepositoryStorage1234567
  1. The client sends a request. The route gives it to its controller.
  2. The controller turns the request into an Input DTO, AddMessageCommand, and sends it on the message bus. When the request is not valid, the controller answers at once: 400, 413, 415 or 422.
  3. The message bus gives the DTO to its handler, AddMessageHandler.
  4. The handler asks the domain: Message.create refuses an empty text, or a text that is too long.
  5. The handler keeps the message through its port, MessageRepository. The adapter of the port, FileMessageRepository, writes the file.
  6. The handler answers an Output DTO, AddMessageOutput, or the error of the domain.
  7. The controller turns the answer into the response: 201 with the message, or a problem+json error.

src/backend/messages/application/command/add-message/add-message.handler.ts

export class AddMessageHandler {
  constructor(private readonly messages: MessageRepository) {}

  async execute(command: AddMessageCommand): Promise<AddMessageOutput> {
    const message = Message.create(await this.messages.nextId(), command.text);
    await this.messages.save(message);
    return new AddMessageOutput(message.id, message.text);
  }
}

Ports and adapters

The domain says what it needs from the outside, with a port: an interface, such as MessageRepository. The infra layer implements it, with adapters: one that keeps the messages in a file, one that keeps them in memory for the tests. This is the hexagonal architecture, also called ports and adapters.

  • The container gives the adapters. A handler names the ports in its constructor. CAITS reads these types at the build, and gives the adapter that di.ts binds: you register nothing. In the tests, test.di.ts replaces the bindings.
  • A contract for each port. The tests of a port are written once, in its contract. Each adapter runs them. So the fake adapter of the tests behaves like the real one. See Add a port.

Between the contexts

Two contexts of the backend never call each other. A handler publishes an event of its domain, and the context map, src/backend/context-map.ts, turns it into the DTOs of the other contexts. The publisher does not wait for them: the other contexts react later, each in its own use case. See Connect two contexts.

The contexts of the frontend do not talk to each other: a page places their views side by side.

The harness

The architecture rules and the tests make the harness. npm run check runs them all at once:

CheckWhat it proves
TypesThe code and the Marko templates have no type error, in strict TypeScript
LintESLint finds no problem
ArchitectureEach file respects the 52 rules: its place, its imports, its shape
Unit testsEach layer has its tests, and they run 100% of the code
Mutation testsThe tests see each small change of the code: 100% of the mutants are killed
End-to-end testsThe app works in a real browser, on its production build

The output of a check that fails names the file and the rule. So a person, or an AI agent, knows what to fix. See When a check fails.

Next pageThe example app The app of init --example, file by file: its path, its design, its tests.