modular-api
Use-case-centric toolkit for building modular APIs with Starlette.
Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and get automatic OpenAPI documentation.
Also available in Dart: modular_api · TypeScript: @macss/modular-api
Quick start
from modular_api import ModularApi, ModuleBuilder
# ─── Module builder (separate file in real projects) ──────────
def build_greetings_module(m: ModuleBuilder) -> None:
m.usecase("hello", HelloWorld)
# ─── Server ───────────────────────────────────────────────────
api = ModularApi(base_path="/api")
api.module("greetings", build_greetings_module)
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.py 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 via PydanticField()Output.status_code— custom HTTP status codes per responseUseCaseException— structured error handling (status_code, message, error_code, details)ModularApi+ModuleBuilder— module registration and routing- Constructor-based unit testing with fake dependency injection
cors_middleware— built-in CORS support- All public endpoints resolve under the configured
base_path. - 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) - Full type annotations with
py.typedmarker (PEP 561)
Plugin host
The public plugin contract is available from the package exports 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 duringbuild()in dependency ordervalidate(host)runs after registration freeze and can abort startupshutdown()runs in reverse setup order on normal shutdown and on partial startup rollback- plugin routes always resolve under the configured
base_path - all three public middleware slots are active with deterministic ordering
from modular_api import ModularApi, Plugin, PluginHost, PluginManifest, PluginRoute
class HelloPlugin(Plugin):
manifest = PluginManifest(
id="acme.hello",
display_name="Hello Plugin",
version="0.1.0",
host_api_version=">=0.1.0 <0.2.0",
)
def setup(self, host: PluginHost) -> None:
host.register_route(
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=lambda _: {
"status": 200,
"body": {"ok": True, "basePath": host.metadata().base_path},
},
)
)
def validate(self, host: PluginHost):
return []
api = ModularApi(base_path="/api")
api.plugin(HelloPlugin())
app = api.build()
Installation
pip install macss-modular-api
With Uvicorn for api.serve():
pip install macss-modular-api[serve]
Error handling
async def execute(self) -> FoundUserOutput:
user = await repository.find_by_id(self.input.user_id)
if not user:
raise UseCaseException(
status_code=404,
message="User not found",
error_code="USER_NOT_FOUND",
)
return FoundUserOutput(name=user.name)
Testing
async def test_hello_world():
usecase = HelloWorld(HelloInput(name="World"))
error = usecase.validate()
assert error is None
output = await usecase.execute()
assert output.message == "Hello, World!"
See doc/testing_guide.md for the full testing guide.
License
MIT — see LICENSE.
Release files for macss-modular-api 0.7.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| macss_modular_api-0.7.1.tar.gz | 94.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| macss_modular_api-0.7.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 197.5 kB
Release files / macss_modular_api-0.7.1.tar.gz
| Download URL | macss_modular_api-0.7.1.tar.gz |
|---|---|
| Size | 94.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aadd38b5b9528cb1cb62857dfddbe7bb99f3de01f18b05762421708828a92958
|
|
BLAKE2b-256 checksum How to use checksums |
3d2b35f65b1624bed2f18f2dd85ecd84429ddb7210f1a4718b750106b5e958e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / macss_modular_api-0.7.1-py3-none-any.whl
| Download URL | macss_modular_api-0.7.1-py3-none-any.whl |
|---|---|
| Size | 102.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e378e84d5e77de0c337ad534bf3c914d8e8353a4ce45d2d356e57fc0f237e7b3
|
|
BLAKE2b-256 checksum How to use checksums |
3f1b473c2ef1f40637b8d14245aeb7227e168fdc88f73bd1b5ec7c0d776806ed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|