Skip to content

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)

1
2
3
4
5
6
7
domain/parents/
    models.py               # framework-free Pydantic model (id optional)
    ports/
        db.py               # ParentDocument(Parent, BaseDocument) + create_parent_repo()
        service.py          # ParentCrudsService(CrudsServiceAdaptor[...]) + get_parent_service()
interfaces/parents/
    routes.py               # list/create/get/update/delete — names == operationIds

Wiring happens in exactly one place — the composition root init.py:

parent_repo = create_parent_repo()
_resources["parent_svc"] = get_parent_service(parent_repo)

Routes resolve the service from app state through the shared helper (interfaces/_common.py):

1
2
3
async def list_parents(request: Request, limit: int = 20, offset: int = 0) -> dict:
    docs = await svc(request, "parent").search({})
    return paginate([dump(d) for d in docs], limit, offset)

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:

pet = await expand_ref(svc(request, "pet"), body.get("pet"), "pet")

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: every operationId resolves 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).