Models
FastApp models are lightweight dataclasses layered on top of Motor. They include change tracking, Mongo-friendly helpers, and Laravel-inspired relationships.
Generating a model
Use the CLI to scaffold a new model. Naming convention: omit the Model suffix in both the class and filename.
fast-app make model User
This generates app/models/user.py with a User class stub. Add field annotations directly on the dataclass:
from fast_app import Model
class User(Model):
email: str
name: str | None = None
Fields declared on the class become persisted attributes. Avoid suffixing the file or class with Model; FastApp handles that implicitly.
Creating and saving
Instantiate the model and call save() to insert or update depending on whether _id is set:
user = User(email="john@example.com", name="John")
await user.save() # inserts a document and assigns user._id
# Update attributes and save again
user.name = "John Smith"
await user.save() # triggers update and refreshes fields
Alternatively, use the convenience class methods:
created = await User.create({"email": "jane@example.com", "name": "Jane"})
first = await User.first() # returns first document
exists = await User.exists({"email": "jane@example.com"}) # True or False
Reading and querying
The contract exposes familiar helpers:
User.find(query)→ list ofUserUser.find_one(query)→ singleUser | NoneUser.find_by_id(id)→ fetch by ObjectId or hex stringUser.find_or_fail(query)/find_by_id_or_fail(id)→ raiseModelNotFoundExceptionon absenceUser.search(query)→ text search across fields and configured relationsUser.scope()→ fluent query builder
Example:
users = await User.find({"active": True}, sort=[("created_at", -1)])
maybe = await User.find_one({"email": "john@example.com"})
by_id = await User.find_by_id("66f2d3...")
if maybe:
await maybe.update({"name": "John Updated"})
count = await User.count({"active": True})
Relationships
Models include three helpers for simple relationships. Define async accessors on the model to make usage explicit and keep circular imports at bay.
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
from fast_app import Model
if TYPE_CHECKING:
from app.models.lead import Lead
class User(Model):
name: str
async def lead(self) -> Optional['Lead']:
from app.models.lead import Lead # local import to avoid circular dependency
return await self.has_one(Lead)
class Lead(Model):
user_id: ObjectId
async def user(self) -> Optional[User]:
return await self.belongs_to(User)
user = await User.find_by_id(user_id)
lead = await user.lead() # has_one helper under the hood
user = await lead.user()
belongs_to(parent_model, parent_key="_id", child_key="modelname_id")has_one(child_model, parent_key="_id", child_key="modelname_id")has_many(child_model, parent_key="_id", child_key="modelname_id")
Override parent_key or child_key for non-standard schemas. Each helper automatically converts string IDs to ObjectId when is_object_id is True (default).
When you expose relationships as methods, use TYPE_CHECKING imports (as above) to keep type hints without triggering runtime import cycles.
Change tracking and persistence
Setting attributes records dirty fields in self.clean. save() or update() writes only changed fields and updates updated_at. Successful operations trigger observer hooks and bump the collection cache version, which invalidates cached queries.
user.set("name", "Alice")
await user.save()
await User.update_many({"active": False}, {"$set": {"active": True}})
Use touch() to bump the updated_at timestamp without modifying other fields.