The api layer of MACSS
Build modular, contract-first APIs — server side
modular_api implements the api element of MACSS. It is simultaneously a methodology (contract-first, use-case driven), a specification (conventions that define how modules, DTOs, repositories and use cases relate), and a set of SDKs in three languages that produce structurally identical openapi.json outputs from the same conceptual model.
Every operation is a UseCase — a unit with typed Input, typed Output, a validate() step and an execute() step. There are no controllers that mix validation, business logic and HTTP concerns. The framework enforces the separation.
CQRS is structural, not optional: commands travel over REST, queries over GraphQL. The OpenAPI spec and the GraphQL schema are generated automatically — they are a consequence of the use cases, not a document you maintain separately.
Every server ships four endpoints with zero configuration: /docs, /health, /openapi.json, /openapi.yaml. Metrics at /metrics are opt-in.
// pubspec.yaml
dependencies:
modular_api: ^1.0.0
// bin/server.dart
import 'package:modular_api/modular_api.dart';
Future<void> main() async {
final api = ModularApi(
basePath: '/api',
title: 'My App',
version: '1.0.0',
);
api.module('clients', (m) {
m.usecase('create', CreateClient.fromJson);
m.usecase('list', ListClients.fromJson);
});
await api.serve(port: 8080);
}
# Commands → REST curl -X POST http://localhost:8080/api/clients/create \ -H "Content-Type: application/json" \ -d '{"name":"Acme"}' # Queries → GraphQL (auto-generated) # Docs → http://localhost:8080/docs # Spec → http://localhost:8080/openapi.json
Every use case follows the same lifecycle — regardless of language:
// Input: validated before execute() is called class CreateClientInput extends Input { final String name; CreateClientInput({required this.name}); @override String? validate() { if (name.isEmpty) return 'name is required'; return null; } factory CreateClientInput.fromJson(Map<String, dynamic> j) => CreateClientInput(name: j['name'] as String); } // Output: serialized as the HTTP response class CreateClientOutput extends Output { final String id; final String name; CreateClientOutput({required this.id, required this.name}); @override Map<String, dynamic> toJson() => {'id': id, 'name': name}; } // UseCase: pure business logic class CreateClient implements UseCase<CreateClientInput, CreateClientOutput> { @override Future<CreateClientOutput> execute(CreateClientInput input) async { final id = await repository.insert(input.name); return CreateClientOutput(id: id, name: input.name); } }