Managing Item Lifecycles with DTOs
To manage the lifecycle of items in this project, you use specialized Data Transfer Objects (DTOs) built with SQLModel. These DTOs separate the database schema from the API interface, allowing you to enforce validation rules and control data exposure at each stage of an item's existence.
Defining the Base Schema
All item-related DTOs and the database model itself inherit from ItemBase. This ensures that core fields like title and description are defined consistently across the application.
# backend/app/models.py
class ItemBase(SQLModel):
title: str = Field(min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=255)
Creating Items
When creating a new item, use the ItemCreate DTO. It inherits from ItemBase and is used in the POST endpoint to validate incoming request data. The API route then converts this DTO into the Item database model, injecting the owner_id from the currently authenticated user.
# backend/app/api/routes/items.py
@router.post("/", response_model=ItemPublic)
def create_item(
*, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate
) -> Any:
"""
Create new item.
"""
# Validate input and add owner_id
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
session.add(item)
session.commit()
session.refresh(item)
return item
Updating Items Partially
For updates, use the ItemUpdate DTO. It overrides the title field to make it optional, enabling partial updates where the client only sends the fields they wish to change.
In the route, use item_in.model_dump(exclude_unset=True) to extract only the fields provided in the request, then apply them to the existing database record using item.sqlmodel_update(update_dict).
# backend/app/api/routes/items.py
@router.put("/{id}", response_model=ItemPublic)
def update_item(
*,
session: SessionDep,
current_user: CurrentUser,
id: uuid.UUID,
item_in: ItemUpdate,
) -> Any:
item = session.get(Item, id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
# Permission check
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=403, detail="Not enough permissions")
# Partial update logic
update_dict = item_in.model_dump(exclude_unset=True)
item.sqlmodel_update(update_dict)
session.add(item)
session.commit()
session.refresh(item)
return item
Reading and Listing Items
To control what data is returned to the client, use ItemPublic and ItemsPublic.
ItemPublic: Includes the base fields plus system-generated fields likeid,owner_id, andcreated_at.ItemsPublic: A wrapper for paginated lists, containing a list ofItemPublicobjects and a totalcount.
# backend/app/api/routes/items.py
@router.get("/", response_model=ItemsPublic)
def read_items(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any:
# ... logic to fetch items based on user role ...
# Convert database models to public DTOs
items_public = [ItemPublic.model_validate(item) for item in items]
return ItemsPublic(data=items_public, count=count)
Troubleshooting: Type Overrides in Updates
When defining ItemUpdate, you may encounter type checker warnings because you are overriding a required field (title) from ItemBase with an optional one. This project handles this using a # type: ignore[assignment] comment to allow the DTO to support partial updates while still inheriting from the base schema.
# backend/app/models.py
class ItemUpdate(ItemBase):
# Overriding title to be optional for partial updates
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore[assignment]