# CRUD Template ## Summary Every resource follows the same four-file slice. Repetition is intentional: it makes the codebase predictable and generatable. --- ## The slice ``` domain// 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//models.py` | **No** | business shape, framework-free | | **Document** | `domain//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 models` produces the HTTP > *contract* (see the [Build Recipe](01_build_recipe.md), step 3). Response > schemas frequently declare a required `id: str`, so a generated class is > **not** a domain model. If you write > `class ItemDocument(Item, BaseDocument)` and `Item` re-exports a required > `id: str`, that field shadows `BaseDocument.id` and no Mongo `_id` can ever be > set. Fix it one of three ways: > > 1. **Preferred:** keep the domain model id-free (hand-author it from the CRUD > template), and keep the required `id` only on the wire model. > 2. Order the bases so the document wins: > `class ItemDocument(BaseDocument, Item)`. > 3. Keep a separate document type and translate with `ports/converters.py`. ## models.py A framework-free Pydantic model describing the business shape (no `id`). ```python 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. ```python # 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/...`. ```python 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 | `Document` | | repository factory | `create__repo` | | service | `Service` | | service factory | `get__service` | | resource key | `_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).