modular_api 0.6.0
modular_api: ^0.6.0 copied to clipboard
Use-case centric API toolkit for Dart — Shelf UseCase base class, HTTP adapters, CORS middleware, and automatic OpenAPI documentation.
modular_api #
Use-case centric toolkit for building modular APIs with Shelf.
Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and get automatic Swagger/OpenAPI documentation.
Also available in TypeScript: @macss/modular-api · Python: macss-modular-api
Quick start #
import 'package:modular_api/modular_api.dart';
// ─── Module builder (separate file in real projects) ──────────
void buildGreetingsModule(ModuleBuilder m) {
m.usecase('hello', HelloWorld.fromJson);
}
// ─── Server ───────────────────────────────────────────────────
Future<void> main() async {
final api = ModularApi(basePath: '/api');
api.module('greetings', buildGreetingsModule);
await api.serve(port: 8080);
}
curl -X POST http://localhost:8080/api/greetings/hello \
-H "Content-Type: application/json" \
-d '{"name":"World"}'
{"message":"Hello, World!"}
Docs → http://localhost:8080/api/docs
Health → http://localhost:8080/api/health
OpenAPI JSON → http://localhost:8080/api/openapi.json (also /api/openapi.yaml)
Metrics → http://localhost:8080/api/metrics (opt-in)
See example/example.dart for the full implementation including Input, Output, UseCase with validate(), and the builder.
Features #
UseCase<I, O>— pure business logic, no HTTP concernsInput/Output— DTOs with automatic OpenAPI schema generation viaschemaFieldsOutput.statusCode— custom HTTP status codes per responseUseCaseException— structured error handling (status code, message, error code, details)ModularApi+ModuleBuilder— module registration and routingcorsMiddleware()— configurable CORS support- All public endpoints resolve under the configured
basePath. - Swagger UI at
/{basePath}/docs— auto-generated from registered use cases - OpenAPI spec at
/{basePath}/openapi.jsonand/{basePath}/openapi.yaml— raw spec download - Health check at
GET /{basePath}/health— IETF Health Check Response Format - Prometheus metrics at
GET /{basePath}/metrics— Prometheus exposition format - Structured JSON logging — Loki/Grafana compatible, request-scoped with trace_id
- All endpoints default to
POST(configurable per use case)
Plugin host #
The public plugin contract is available from package:modular_api/modular_api.dart
and is already used by the official health, metrics, OpenAPI, and docs plugins.
Current lifecycle behavior:
api.plugin(...)registers a plugin instance without running setup yetsetup(host)runs duringserve()in dependency orderValidatingPlugin.validate(host)runs after registration freeze and can abort startupShutdownAwarePlugin.shutdown()runs in reverse setup order on normal shutdown and on partial startup rollback- plugin routes always resolve under the configured
basePath - all three public middleware slots are active with deterministic ordering
import 'dart:convert';
import 'package:modular_api/modular_api.dart';
class HelloPlugin implements Plugin, ValidatingPlugin {
@override
final manifest = const PluginManifest(
id: 'acme.hello',
displayName: 'Hello Plugin',
version: '0.1.0',
hostApiVersion: '>=0.1.0 <0.2.0',
);
@override
void setup(PluginHost host) {
host.registerRoute(
PluginRoute(
id: 'hello-plugin',
method: 'GET',
path: '/hello-plugin',
visibility: 'custom',
// Optional OpenAPI Operation object — when present, the official
// OpenApiPlugin merges the route into /openapi.json and /docs (ADR-0003).
openapi: {
'summary': 'Hello from a plugin route',
'responses': {
'200': {'description': 'OK'},
},
},
handler: (_) => Response.ok(
jsonEncode({'ok': true, 'basePath': host.metadata().basePath}),
headers: {'content-type': 'application/json'},
),
),
);
}
@override
List<PluginValidationResult> validate(PluginHost host) => const [];
}
Installation #
dependencies:
modular_api: ^0.5.0
dart pub add modular_api
Error handling #
@override
Future<GetUserOutput> execute() async {
final user = await repository.findById(input.userId);
if (user == null) {
throw UseCaseException(
statusCode: 404,
message: 'User not found',
errorCode: 'USER_NOT_FOUND',
);
}
return GetUserOutput(user: user);
}
{"error": "USER_NOT_FOUND", "message": "User not found"}
Testing #
import 'package:test/test.dart';
void main() {
test('HelloWorld returns greeting', () async {
final useCase = HelloWorld(HelloInput(name: 'World'));
expect(useCase.validate(), isNull);
final output = await useCase.execute();
expect(output.message, 'Hello, World!');
});
}
dart test
Architecture #
HTTP Request → ModularApi → Module → UseCase → Business Logic → Output → HTTP Response
- UseCase layer — pure logic, independent of HTTP
- HTTP adapter — turns a UseCase into a Shelf Handler
- Middlewares — cross-cutting concerns (CORS, logging)
- Swagger UI — documentation served automatically
Documentation #
- AGENTS.md — Framework guide (AI-optimized)
- doc/INDEX.md — Documentation index
- doc/usecase_dto_guide.md — Creating Input/Output DTOs
- doc/usecase_implementation.md — Implementing UseCases
- doc/testing_guide.md — Testing guide
- doc/health_check_guide.md — Health check endpoint
- doc/metrics_guide.md — Prometheus metrics endpoint
- doc/logger_guide.md — Structured JSON logger
Compile to executable #
dart compile exe bin/main.dart -o build/server
License #
MIT © ccisne.dev