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.
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.
// 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); }
// 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.