Domain & API — the resource axis
Every aggregate follows the same four-file slice
(CRUD Template); walk parents first, then
appointments when you need foreign keys.
The slice, end to end (parents)
Wiring happens in exactly one place — the composition root
init.py:
Routes resolve the service from app state through the shared helper
(interfaces/_common.py):
The spec is the contract
openapi.yaml is OpenAPI 3.1 and the
single source of truth: main.py builds an OpenAPIFirstApp that binds each
operationId to a function of the same name in interfaces/openapi.py (a flat
re-export of every routes module). There are no decorator routers.
The x- extension fields on schemas and properties are what make the admin
client render itself — see Auto-Rendered Admin and
the worked Job schema (enum selects, hidden fields, filterable columns).
Foreign keys (appointments)
Appointment declares pet, vet, treatment as dict | int | None. The
write path normalizes bare ids through expand_ref; the read path returns
fully expanded objects:
The list endpoint additionally filters by date prefix and embedded fk ids — client-side over the repository result, the pattern for small reference data.
Ids on the wire
The domain models declare id: int | None = None and documents use the
integer-id repository variants (infra/db/repos/mongo_int.py,
create_repo() in domain/*/ports/db.py, REPO_BACKEND=memory|mongo). This
keeps the OpenAPI id: integer contract, the in-memory demo store, and the
Mongo persistence layer consistent.
Tests
tests/integration/test_openapi_app_builds.py— generic gate: everyoperationIdresolves and the app builds. Copy it verbatim.tests/unit/test_jobs_routes.py— the pattern for route unit tests with a service double (no HTTP client needed).