uiWater

service_client

The ui layer of MACSS

Transport-agnostic service client with the Result pattern

service_client implements the ui element of MACSS. It sits between the presentation layer and the API — abstracting transport so controllers never import http, never catch raw exceptions, and never parse JSON manually. The contract is a typed Result: either a value or a structured error.

Philosophy

In MACSS the UI layer is water — it adapts to its container (Flutter app, React SPA, CLI, another API) without changing shape. What must not change is how it communicates with the API: always through a service class, always receiving a Result, never crashing on network failure.

The Result pattern eliminates the try / catch noise from controllers. A Result<T> is either Ok(value) or Err(error) — exhaustively handled at the call site. The controller decides what to show; the service client decides nothing about presentation.

The interface is transport-agnostic. The default implementation uses HTTP, but any transport can be plugged in — useful for testing, for mocking, or for communicating between modules in-process.

Quick start Dart · also available in TS and Python

// pubspec.yaml
dependencies:
  service_client: ^0.2.1
// lib/clients/service.dart
import 'package:service_client/service_client.dart';

class ClientsService {
  final ServiceClient _client;
  ClientsService(this._client);

  Future<Result<CreateClientOutput>> create(String name) =>
      _client.post(
        '/api/clients/create',
        body: {'name': name},
        fromJson: CreateClientOutput.fromJson,
      );
}
// lib/clients/controller.dart
final result = await service.create(nameInput);

switch (result) {
  case Ok(:final value):
    // update UI state with value
    state = ClientCreated(value);
  case Err(:final error):
    // show error — no try/catch needed
    state = ClientError(error.message);
}

Wiring it up

// main.dart — inject once, use everywhere
final client = HttpServiceClient(
  baseUrl: 'http://localhost:8080',
  headers: {'Authorization': 'Bearer $token'},
);

final clientsService = ClientsService(client);
final clientsController = ClientsController(clientsService);

For tests, replace HttpServiceClient with a MockServiceClient — no HTTP calls, no test server needed. The controller code does not change.