fastapi-core-models
SKILL.md
FastAPI Core Models
Overview
This skill covers creating the base SQLAlchemy model and reusable mixins for UUID primary keys, timestamps, and soft delete functionality.
Create core/models.py
Create src/app/core/models.py:
from datetime import datetime
from uuid import UUID, uuid4
from sqlalchemy import DateTime, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs
class Base(AsyncAttrs, DeclarativeBase):
"""
Base class for all SQLAlchemy models.
Includes AsyncAttrs for proper async lazy loading support.
All models should inherit from this class.
"""
@declared_attr.directive
def __tablename__(cls) -> str:
"""
Generate table name from class name.
Converts CamelCase to snake_case and pluralizes.
Example: UserProfile -> user_profiles
"""
import re
name = re.sub(r"(?<!^)(?=[A-Z])", "_", cls.__name__).lower()
return f"{name}s"
class UUIDMixin:
"""
Mixin that adds a UUID primary key.
Uses PostgreSQL's native UUID type for optimal storage and indexing.
Generates UUID4 by default.
"""
id: Mapped[UUID] = mapped_column(
PG_UUID(as_uuid=True),
primary_key=True,
default=uuid4,
sort_order=-100, # Ensure id appears first in table
)
class TimestampMixin:
"""
Mixin that adds created_at and updated_at timestamps.
- created_at: Set once when record is created (server-side default)
- updated_at: Updated automatically on every modification
All timestamps are timezone-aware UTC.
"""
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
sort_order=100,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
sort_order=101,
)
class SoftDeleteMixin:
"""
Mixin that adds soft delete functionality.
- deleted_at: NULL means not deleted, timestamp means deleted
- Records are never physically deleted, only marked
Repositories should filter out soft-deleted records by default.
"""
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
default=None,
index=True, # Index for efficient filtering
sort_order=102,
)
@property
def is_deleted(self) -> bool:
"""Check if the record is soft deleted."""
return self.deleted_at is not None
Usage Example
When creating entity models, combine the mixins:
# src/app/items/models.py
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.models import Base, UUIDMixin, TimestampMixin, SoftDeleteMixin
class Item(UUIDMixin, TimestampMixin, SoftDeleteMixin, Base):
"""Item model with UUID, timestamps, and soft delete."""
__tablename__ = "items" # Explicit table name (optional)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
Generated Table Structure
The above model generates:
CREATE TABLE items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX ix_items_deleted_at ON items (deleted_at);
Mixin Order Matters
Always use this order when inheriting:
class MyModel(UUIDMixin, TimestampMixin, SoftDeleteMixin, Base):
This ensures:
- UUID
idappears first (sort_order=-100) - Entity columns appear in the middle
- Timestamps appear last (sort_order=100-102)
Type Annotations
SQLAlchemy 2.0 uses Mapped[] for all column definitions:
# Required field
name: Mapped[str] = mapped_column(String(255))
# Optional field
description: Mapped[str | None] = mapped_column(Text, nullable=True)
# With default
is_active: Mapped[bool] = mapped_column(default=True)
# Foreign key
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"))
Common Column Types
from sqlalchemy import (
String, # VARCHAR(n)
Text, # TEXT (unlimited)
Integer, # INTEGER
BigInteger, # BIGINT
Float, # FLOAT
Numeric, # DECIMAL(precision, scale)
Boolean, # BOOLEAN
DateTime, # TIMESTAMP
Date, # DATE
JSON, # JSONB (PostgreSQL)
Enum, # ENUM type
)
from sqlalchemy.dialects.postgresql import (
UUID, # UUID
ARRAY, # ARRAY type
JSONB, # JSONB (explicit)
)
Relationships
from sqlalchemy.orm import relationship
class User(UUIDMixin, TimestampMixin, Base):
__tablename__ = "users"
name: Mapped[str] = mapped_column(String(255))
# One-to-many relationship
items: Mapped[list["Item"]] = relationship(
back_populates="user",
lazy="selectin", # Async-safe loading
)
class Item(UUIDMixin, TimestampMixin, Base):
__tablename__ = "items"
name: Mapped[str] = mapped_column(String(255))
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"))
# Many-to-one relationship
user: Mapped["User"] = relationship(back_populates="items")
Async-Safe Relationship Loading
For async contexts, use these loading strategies:
| Strategy | Use Case |
|---|---|
lazy="selectin" |
Load related objects in separate SELECT |
lazy="joined" |
Load with JOIN (use sparingly) |
lazy="raise" |
Raise error if accessed (explicit loading only) |
Avoid lazy="select" (default) - it triggers implicit I/O in async context.
Indexing
from sqlalchemy import Index
class Item(UUIDMixin, TimestampMixin, Base):
__tablename__ = "items"
name: Mapped[str] = mapped_column(String(255), index=True)
status: Mapped[str] = mapped_column(String(50))
category: Mapped[str] = mapped_column(String(100))
# Composite index
__table_args__ = (
Index("ix_items_status_category", "status", "category"),
)
Weekly Installs
1
Repository
agusmdev/burntopGitHub Stars
3
First Seen
7 days ago
Security Audits
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1