For the complete documentation index, see llms.txt. This page is also available as Markdown.

Service

A service is an outbound integration — a client for an external system (a REST API, email, payments, a message broker). Like a repository, it sits behind a port: your use cases depend on the tech-free contract, and a concrete adapter makes the call. Swap providers by swapping the adapter; the domain never changes.

Port — the contract + DI token

Define the port as an abstract class with a static Token, so the type and the DI token share one name and handlers never import the adapter:

import { Result } from '@soapjs/soap';

export abstract class WeatherService {
  static readonly Token = 'WeatherService';
  abstract getByCity(city: string): Promise<Result<Weather>>;
}

Adapter — the implementation

The adapter performs the actual call and maps the response into your domain, returning a Result. It lives in the feature's data/ folder and is the only place that knows the external API:

import { Result, Failure } from '@soapjs/soap';

export class OpenWeatherService implements WeatherService {
  constructor(private readonly apiKey: string) {}

  async getByCity(city: string): Promise<Result<Weather>> {
    try {
      const res = await fetch(`https://api.example.com/weather?q=${city}&key=${this.apiKey}`);
      if (!res.ok) return Result.withFailure(Failure.fromError(new Error(`Weather API ${res.status}`)));
      const data = await res.json();
      return Result.withSuccess(new Weather(data.city, data.tempC));
    } catch (error) {
      return Result.withFailure(Failure.fromError(error));
    }
  }
}

Wire it in the composition root

Inject the port wherever you need it — a use case or a command handler:

Service vs Repository

Both are outbound ports + adapters; the difference is intent:

Repository
Service

Talks to

your own data store

an external system / API

Returns

your entities

data mapped into your domain

Built on

ReadWriteRepository + a Source

a plain class (or any client)

Because a service is just a port, it's trivial to stub in tests — bind an in-memory fake under the same Token.

Last updated