P0: fix doc:// -> docs:// across standards src/server/registry and regenerate stale mongo-ops bundle. P1: build recipe + spec-first routing now document the openapi-first codegen fast path (models/routes with --inject-request/--package). P4: CRUD template documents the model-vs-document id rule (BaseDocument owns id). P5: authless guidance (omit security keys). P6: template gains generic infra (http_cache, keyed SSE bus, KV port+in-memory+Redis), interfaces/openapi.py + .dockerignore + generic build test registered as components, and a get_app_template tool; genericity matrix marks LLM pool + human-in-loop as reference-app patterns. P7: intake/build/start_app/trial prompts. P8: template ships tests/integration/test_openapi_app_builds.py. Rebuilt standards + openapi-first bundles and re-collected.
3.4 KiB
CRUD Template
Summary
Every resource follows the same four-file slice. Repetition is intentional: it makes the codebase predictable and generatable.
The slice
domain/<resource>/
models.py # pure Pydantic model (no DB fields)
ports/
db.py # document model + repository factory
service.py # service class + factory
converters.py # (optional) transforms
The id rule (model vs. document)
Two different objects, two different jobs:
| Object | Lives in | Has id? |
Purpose |
|---|---|---|---|
| Domain model | domain/<resource>/models.py |
No | business shape, framework-free |
| Document | domain/<resource>/ports/db.py |
Yes (from BaseDocument) |
Mongo persistence shape |
BaseDocument (mongo-ops) supplies id: Optional[PyObjectId] (aliased to
_id) plus created_at / updated_at. The domain model must not declare
id; Mongo owns it.
Trap — generated wire models.
openapi-first modelsproduces the HTTP contract (see the Build Recipe, step 3). Response schemas frequently declare a requiredid: str, so a generated class is not a domain model. If you writeclass ItemDocument(Item, BaseDocument)andItemre-exports a requiredid: str, that field shadowsBaseDocument.idand no Mongo_idcan ever be set. Fix it one of three ways:
- Preferred: keep the domain model id-free (hand-author it from the CRUD template), and keep the required
idonly on the wire model.- Order the bases so the document wins:
class ItemDocument(BaseDocument, Item).- Keep a separate document type and translate with
ports/converters.py.
models.py
A framework-free Pydantic model describing the business shape (no id).
class Item(BaseModel):
name: str
quantity: int = 0
ports/db.py
The persistence model extends the business model and adds database concerns. The repository factory selects the right repository flavour.
# Item is id-free; BaseDocument adds id/created_at/updated_at.
class ItemDocument(Item, BaseDocument):
pass
def create_item_repo(*, owner_repo=None) -> ItemRepository:
return repo_factory(
collection="items",
document=ItemDocument,
populate_rules=[PopulateRule(field="owner", collection="users")],
cache=None, # or a CachedBaseRepository config
)
ports/service.py
The service is built through the DI library, injecting the repository. The
generic CRUD base provides create/read/update/patch/delete/search/....
class ItemService(CrudsServiceAdaptor[Item, ItemDocument]):
model = Item
schema = ItemDocument
def get_item_service(repo) -> ItemService:
return build(ItemService, instances={"repo": repo})
Naming conventions
| Concept | Convention |
|---|---|
| collection | plural snake case (items) |
| document | <Resource>Document |
| repository factory | create_<resource>_repo |
| service | <Resource>Service |
| service factory | get_<resource>_service |
| resource key | <resource>_svc |
| operationId | list_items, create_item, ... |
Optional files
converters.py— pure functions mapping between domain types.ports/enrichment.py— adapts a pipeline into an async callable the service can invoke (used when a resource is populated by a pipeline).