Common Patterns¶
This guide demonstrates common real-world patterns and scenarios you'll encounter when building admin interfaces with CRUDAdmin. Each pattern includes complete examples with models, schemas, and configuration.
Prerequisites¶
- Basic understanding of Adding Models
- Familiarity with Admin Interface operations
- Knowledge of SQLAlchemy relationships and Pydantic schemas
Multi-Model Relationships¶
Blog System Pattern¶
A common pattern is managing content with related models (users, posts, comments, tags).
Complete Blog System Models
# models.py
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Boolean, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
Base = declarative_base()
# Many-to-many association table
post_tags = Table('post_tags', Base.metadata,
Column('post_id', Integer, ForeignKey('posts.id')),
Column('tag_id', Integer, ForeignKey('tags.id'))
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True)
email = Column(String(100), unique=True, index=True)
full_name = Column(String(100))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
posts = relationship("Post", back_populates="author")
comments = relationship("Comment", back_populates="author")
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), unique=True)
description = Column(Text)
# Relationships
posts = relationship("Post", back_populates="category")
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(30), unique=True)
color = Column(String(7)) # Hex color code
# Relationships
posts = relationship("Post", secondary=post_tags, back_populates="tags")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200))
content = Column(Text)
excerpt = Column(String(500))
author_id = Column(Integer, ForeignKey("users.id"))
category_id = Column(Integer, ForeignKey("categories.id"))
published = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
author = relationship("User", back_populates="posts")
category = relationship("Category", back_populates="posts")
comments = relationship("Comment", back_populates="post")
tags = relationship("Tag", secondary=post_tags, back_populates="posts")
class Comment(Base):
__tablename__ = "comments"
id = Column(Integer, primary_key=True, index=True)
content = Column(Text)
author_id = Column(Integer, ForeignKey("users.id"))
post_id = Column(Integer, ForeignKey("posts.id"))
approved = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
author = relationship("User", back_populates="comments")
post = relationship("Post", back_populates="comments")
Pydantic Schemas for Blog System
# schemas.py
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
from typing import Optional, List
# User Schemas
class UserBase(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
full_name: str = Field(..., max_length=100)
is_active: bool = True
class UserCreate(UserBase):
pass
class UserUpdate(UserBase):
username: Optional[str] = Field(None, min_length=3, max_length=50)
email: Optional[EmailStr] = None
full_name: Optional[str] = Field(None, max_length=100)
is_active: Optional[bool] = None
class UserRead(UserBase):
id: int
created_at: datetime
class Config:
from_attributes = True
# Category Schemas
class CategoryBase(BaseModel):
name: str = Field(..., max_length=50)
description: Optional[str] = None
class CategoryCreate(CategoryBase):
pass
class CategoryUpdate(CategoryBase):
name: Optional[str] = Field(None, max_length=50)
class CategoryRead(CategoryBase):
id: int
class Config:
from_attributes = True
# Tag Schemas
class TagBase(BaseModel):
name: str = Field(..., max_length=30)
color: str = Field(..., regex=r'^#[0-9A-Fa-f]{6}$')
class TagCreate(TagBase):
pass
class TagUpdate(TagBase):
name: Optional[str] = Field(None, max_length=30)
color: Optional[str] = Field(None, regex=r'^#[0-9A-Fa-f]{6}$')
class TagRead(TagBase):
id: int
class Config:
from_attributes = True
# Post Schemas
class PostBase(BaseModel):
title: str = Field(..., max_length=200)
content: str
excerpt: Optional[str] = Field(None, max_length=500)
category_id: int
published: bool = False
class PostCreate(PostBase):
author_id: int
class PostUpdate(PostBase):
title: Optional[str] = Field(None, max_length=200)
content: Optional[str] = None
category_id: Optional[int] = None
published: Optional[bool] = None
class PostRead(PostBase):
id: int
author_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# Comment Schemas
class CommentBase(BaseModel):
content: str
class CommentCreate(CommentBase):
author_id: int
post_id: int
class CommentUpdate(CommentBase):
content: Optional[str] = None
approved: Optional[bool] = None
class CommentRead(CommentBase):
id: int
author_id: int
post_id: int
approved: bool
created_at: datetime
class Config:
from_attributes = True
Registration Pattern¶
from crudadmin import CRUDAdmin, SessionConfig
from models import User, Category, Tag, Post, Comment
from schemas import (
UserCreate, UserUpdate, UserRead,
CategoryCreate, CategoryUpdate, CategoryRead,
TagCreate, TagUpdate, TagRead,
PostCreate, PostUpdate, PostRead,
CommentCreate, CommentUpdate, CommentRead
)
# Initialize CRUDAdmin
crud_admin = CRUDAdmin(
session=get_session,
SECRET_KEY="your-secret-key-here",
sessions=SessionConfig(backend="database"),
)
# Register models in logical order
crud_admin.add_view(
model=User,
create_schema=UserCreate,
update_schema=UserUpdate,
select_schema=UserRead
)
crud_admin.add_view(
model=Category,
create_schema=CategoryCreate,
update_schema=CategoryUpdate,
select_schema=CategoryRead
)
crud_admin.add_view(
model=Tag,
create_schema=TagCreate,
update_schema=TagUpdate,
select_schema=TagRead
)
crud_admin.add_view(
model=Post,
create_schema=PostCreate,
update_schema=PostUpdate,
select_schema=PostRead
)
crud_admin.add_view(
model=Comment,
create_schema=CommentCreate,
update_schema=CommentUpdate,
select_schema=CommentRead
)
Why This Pattern Works¶
- Clear hierarchy: Users → Categories/Tags → Posts → Comments
- Manageable complexity: Each model has focused responsibility
- Relationship visibility: Foreign key fields show in forms
- Data integrity: Relationships enforce referential integrity
E-commerce Management Pattern¶
Product Catalog System¶
Managing products, inventory, orders, and customers in an e-commerce admin.
E-commerce Models
# ecommerce_models.py
from sqlalchemy import Column, Integer, String, Decimal, ForeignKey, DateTime, Boolean, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
from decimal import Decimal as PyDecimal
Base = declarative_base()
class Customer(Base):
__tablename__ = "customers"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(100), unique=True, index=True)
first_name = Column(String(50))
last_name = Column(String(50))
phone = Column(String(20))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
orders = relationship("Order", back_populates="customer")
class ProductCategory(Base):
__tablename__ = "product_categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True)
description = Column(Text)
is_active = Column(Boolean, default=True)
# Relationships
products = relationship("Product", back_populates="category")
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
sku = Column(String(50), unique=True, index=True)
name = Column(String(200))
description = Column(Text)
price = Column(Decimal(10, 2))
cost = Column(Decimal(10, 2))
category_id = Column(Integer, ForeignKey("product_categories.id"))
stock_quantity = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
category = relationship("ProductCategory", back_populates="products")
order_items = relationship("OrderItem", back_populates="product")
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
order_number = Column(String(20), unique=True, index=True)
customer_id = Column(Integer, ForeignKey("customers.id"))
status = Column(String(20), default="pending") # pending, processing, shipped, delivered, cancelled
total_amount = Column(Decimal(10, 2))
shipping_address = Column(Text)
order_date = Column(DateTime, default=datetime.utcnow)
shipped_date = Column(DateTime, nullable=True)
# Relationships
customer = relationship("Customer", back_populates="orders")
items = relationship("OrderItem", back_populates="order")
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(Integer, primary_key=True, index=True)
order_id = Column(Integer, ForeignKey("orders.id"))
product_id = Column(Integer, ForeignKey("products.id"))
quantity = Column(Integer)
unit_price = Column(Decimal(10, 2))
total_price = Column(Decimal(10, 2))
# Relationships
order = relationship("Order", back_populates="items")
product = relationship("Product", back_populates="order_items")
E-commerce Schemas
# ecommerce_schemas.py
from pydantic import BaseModel, EmailStr, Field, field_validator
from datetime import datetime
from typing import Optional
from decimal import Decimal
from enum import Enum
class OrderStatus(str, Enum):
pending = "pending"
processing = "processing"
shipped = "shipped"
delivered = "delivered"
cancelled = "cancelled"
# Customer Schemas
class CustomerBase(BaseModel):
email: EmailStr
first_name: str = Field(..., max_length=50)
last_name: str = Field(..., max_length=50)
phone: Optional[str] = Field(None, max_length=20)
is_active: bool = True
class CustomerCreate(CustomerBase):
pass
class CustomerUpdate(CustomerBase):
email: Optional[EmailStr] = None
first_name: Optional[str] = Field(None, max_length=50)
last_name: Optional[str] = Field(None, max_length=50)
class CustomerRead(CustomerBase):
id: int
created_at: datetime
class Config:
from_attributes = True
# Product Category Schemas
class ProductCategoryBase(BaseModel):
name: str = Field(..., max_length=100)
description: Optional[str] = None
is_active: bool = True
class ProductCategoryCreate(ProductCategoryBase):
pass
class ProductCategoryUpdate(ProductCategoryBase):
name: Optional[str] = Field(None, max_length=100)
is_active: Optional[bool] = None
class ProductCategoryRead(ProductCategoryBase):
id: int
class Config:
from_attributes = True
# Product Schemas
class ProductBase(BaseModel):
sku: str = Field(..., max_length=50)
name: str = Field(..., max_length=200)
description: Optional[str] = None
price: Decimal = Field(..., gt=0, decimal_places=2)
cost: Decimal = Field(..., ge=0, decimal_places=2)
category_id: int
stock_quantity: int = Field(..., ge=0)
is_active: bool = True
@field_validator('price', 'cost')
@classmethod
def validate_price(cls, v):
if v <= 0:
raise ValueError('Price and cost must be positive')
return v
class ProductCreate(ProductBase):
pass
class ProductUpdate(ProductBase):
sku: Optional[str] = Field(None, max_length=50)
name: Optional[str] = Field(None, max_length=200)
price: Optional[Decimal] = Field(None, gt=0, decimal_places=2)
cost: Optional[Decimal] = Field(None, ge=0, decimal_places=2)
category_id: Optional[int] = None
stock_quantity: Optional[int] = Field(None, ge=0)
is_active: Optional[bool] = None
class ProductRead(ProductBase):
id: int
created_at: datetime
class Config:
from_attributes = True
# Order Schemas
class OrderBase(BaseModel):
customer_id: int
status: OrderStatus = OrderStatus.pending
shipping_address: str
class OrderCreate(OrderBase):
order_number: str = Field(..., max_length=20)
total_amount: Decimal = Field(..., ge=0, decimal_places=2)
class OrderUpdate(BaseModel):
status: Optional[OrderStatus] = None
shipping_address: Optional[str] = None
shipped_date: Optional[datetime] = None
class OrderRead(OrderBase):
id: int
order_number: str
total_amount: Decimal
order_date: datetime
shipped_date: Optional[datetime]
class Config:
from_attributes = True
E-commerce Admin Setup¶
from crudadmin import CRUDAdmin
from ecommerce_models import Customer, ProductCategory, Product, Order, OrderItem
from ecommerce_schemas import (
CustomerCreate, CustomerUpdate, CustomerRead,
ProductCategoryCreate, ProductCategoryUpdate, ProductCategoryRead,
ProductCreate, ProductUpdate, ProductRead,
OrderCreate, OrderUpdate, OrderRead
)
# Configure for e-commerce scale
from crudadmin import CRUDAdmin, RedisConfig, SessionConfig
crud_admin = CRUDAdmin(
session=get_session,
SECRET_KEY="your-ecommerce-secret-key",
sessions=SessionConfig(
backend="redis", redis=RedisConfig(url="redis://localhost:6379")
),
)
# Register in business workflow order
crud_admin.add_view(
model=ProductCategory,
create_schema=ProductCategoryCreate,
update_schema=ProductCategoryUpdate,
select_schema=ProductCategoryRead
)
crud_admin.add_view(
model=Product,
create_schema=ProductCreate,
update_schema=ProductUpdate,
select_schema=ProductRead
)
crud_admin.add_view(
model=Customer,
create_schema=CustomerCreate,
update_schema=CustomerUpdate,
select_schema=CustomerRead
)
crud_admin.add_view(
model=Order,
create_schema=OrderCreate,
update_schema=OrderUpdate,
select_schema=OrderRead
)
Why This E-commerce Pattern Works¶
- Business logic validation: Price/cost validation in schemas
- Enum handling: Order status as proper enums
- Decimal precision: Proper handling of money values
- Scalable session backend: Redis for high traffic
Role-Based Access Pattern¶
CRUDAdmin has two built-in roles. Superusers manage admin accounts, see the event log and every admin's sessions. Regular admins work with your models according to each view's allowed_actions, and see only their own sessions. See Managing Admin Users.
allowed_actions applies to every admin alike. When different groups need different access to the same models, give each group its own admin instance, mounted at its own path, with its own admin database:
from crudadmin import CRUDAdmin
def admin_instance(mount_path: str, admin_db_path: str) -> CRUDAdmin:
return CRUDAdmin(
session=get_session,
SECRET_KEY=os.environ["ADMIN_SECRET_KEY"],
mount_path=mount_path,
admin_db_path=admin_db_path,
)
content_admin = admin_instance("/content", "./admin_content.db")
content_admin.add_view(
model=Post,
create_schema=PostCreate,
update_schema=PostUpdate,
)
content_admin.add_view(
model=Comment,
create_schema=CommentCreate,
update_schema=CommentModerationUpdate,
allowed_actions={"view", "update", "delete"},
)
service_admin = admin_instance("/service", "./admin_service.db")
service_admin.add_view(
model=Order,
create_schema=OrderCreate,
update_schema=OrderStatusUpdate,
allowed_actions={"view", "update"},
)
app.mount("/content", content_admin.app)
app.mount("/service", service_admin.app)
Call initialize() and shutdown() on each in your lifespan. The instances keep separate admin accounts, their session cookies are scoped to their own paths, and they can share one Redis: each stores its keys under its own prefix (see Session Backends).
Advanced Validation Patterns¶
Business Logic Validation¶
Complex validation rules that go beyond basic field validation.
Inventory Management Validation¶
from pydantic import BaseModel, field_validator, model_validator
from typing import Optional, Dict, Any
from typing_extensions import Self
class ProductUpdateAdvanced(BaseModel):
name: Optional[str] = None
price: Optional[Decimal] = None
cost: Optional[Decimal] = None
stock_quantity: Optional[int] = None
is_active: Optional[bool] = None
@field_validator('price')
@classmethod
def price_must_be_reasonable(cls, v):
if v is not None and v > 10000:
raise ValueError('Price cannot exceed $10,000')
return v
@field_validator('stock_quantity')
@classmethod
def stock_cannot_be_negative(cls, v):
if v is not None and v < 0:
raise ValueError('Stock quantity cannot be negative')
return v
@model_validator(mode='after')
def price_above_cost(self) -> Self:
if self.price is not None and self.cost is not None:
if self.price <= self.cost:
raise ValueError('Price must be greater than cost')
return self
@model_validator(mode='after')
def deactivate_out_of_stock(self) -> Self:
if self.stock_quantity is not None and self.stock_quantity == 0 and self.is_active is True:
raise ValueError('Cannot activate product with zero stock')
return self
Order Processing Validation¶
from pydantic import BaseModel, field_validator, model_validator
from typing import Optional
from typing_extensions import Self
from datetime import datetime, timedelta
class OrderUpdateAdvanced(BaseModel):
status: Optional[OrderStatus] = None
shipped_date: Optional[datetime] = None
tracking_number: Optional[str] = None
@field_validator('shipped_date')
@classmethod
def shipped_date_reasonable(cls, v):
if v is not None:
if v < datetime.utcnow().replace(tzinfo=None):
raise ValueError('Shipped date cannot be in the past')
if v > datetime.utcnow().replace(tzinfo=None) + timedelta(days=30):
raise ValueError('Shipped date too far in future')
return v
@model_validator(mode='after')
def status_transition_rules(self) -> Self:
if self.status == OrderStatus.shipped:
if not self.shipped_date:
raise ValueError('Shipped orders must have shipped date')
if not self.tracking_number:
raise ValueError('Shipped orders must have tracking number')
if self.status == OrderStatus.delivered:
if not self.shipped_date:
raise ValueError('Delivered orders must have been shipped first')
return self
Performance Optimization Patterns¶
Large Dataset Management¶
Optimizing CRUDAdmin for applications with millions of records.
Pagination Strategy¶
# Configure for large datasets
crud_admin = CRUDAdmin(
session=get_session,
SECRET_KEY="your-key",
sessions=SessionConfig(backend="redis", timeout_minutes=30),
)
# Enable database indexes in your models
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
sku = Column(String(50), unique=True, index=True) # Indexed for fast lookup
name = Column(String(200), index=True) # Indexed for search
category_id = Column(Integer, ForeignKey("categories.id"), index=True) # Indexed FK
price = Column(Decimal(10, 2), index=True) # Indexed for sorting
is_active = Column(Boolean, default=True, index=True) # Indexed for filtering
created_at = Column(DateTime, default=datetime.utcnow, index=True) # Indexed for sorting
Memory-Efficient Schema Patterns¶
# Lightweight read schemas for list views
class ProductListRead(BaseModel):
id: int
sku: str
name: str
price: Decimal
is_active: bool
class Config:
from_attributes = True
# Detailed schema for individual record views
class ProductDetailRead(BaseModel):
id: int
sku: str
name: str
description: str
price: Decimal
cost: Decimal
category_id: int
stock_quantity: int
is_active: bool
created_at: datetime
class Config:
from_attributes = True
# Use lightweight schema for list operations
crud_admin.add_view(
model=Product,
create_schema=ProductCreate,
update_schema=ProductUpdate,
select_schema=ProductListRead # Faster list loading
)
Integration Patterns¶
Existing FastAPI Application¶
Integrating CRUDAdmin into an existing FastAPI application without conflicts.
Modular Integration¶
# main.py - Your existing FastAPI app
from fastapi import FastAPI
from your_app.routers import api_router
from admin.setup import setup_admin
# Your existing app
app = FastAPI(title="Your API")
# Your existing routes
app.include_router(api_router, prefix="/api/v1")
# Add admin interface
admin_app = setup_admin()
app.mount("/admin", admin_app)
# Your existing startup/shutdown events
@app.on_event("startup")
async def startup():
# Your existing startup code
pass
# admin/setup.py - Separate admin configuration
from crudadmin import CRUDAdmin, SessionConfig
from your_app.models import User, Product, Order
from admin.schemas import AdminUserRead, AdminProductRead, AdminOrderRead
def setup_admin():
"""Configure and return admin application"""
crud_admin = CRUDAdmin(
session=get_session,
sessions=SessionConfig(backend="database"),
admin_db_url="sqlite+aiosqlite:///./admin.db",
SECRET_KEY="admin-secret-key",
)
# Register your models
crud_admin.add_view(
model=User,
create_schema=UserCreate,
update_schema=UserUpdate,
select_schema=AdminUserRead
)
crud_admin.add_view(
model=Product,
create_schema=ProductCreate,
update_schema=ProductUpdate,
select_schema=AdminProductRead
)
crud_admin.add_view(
model=Order,
create_schema=OrderCreate,
update_schema=OrderUpdate,
select_schema=AdminOrderRead
)
return crud_admin.app
Environment-Based Configuration¶
# admin/config.py
import os
from typing import Optional
class AdminConfig:
SECRET_KEY: str = os.getenv("ADMIN_SECRET_KEY", "change-this-in-production")
SESSION_BACKEND: str = os.getenv("ADMIN_SESSION_BACKEND", "database")
REDIS_URL: Optional[str] = os.getenv("ADMIN_REDIS_URL")
DATABASE_URL: str = os.getenv("ADMIN_DATABASE_URL", "sqlite+aiosqlite:///./admin.db")
TITLE: str = os.getenv("ADMIN_TITLE", "Admin Panel")
DEBUG: bool = os.getenv("ADMIN_DEBUG", "false").lower() == "true"
# Use in setup
def setup_admin():
from crudadmin import CRUDAdmin, RedisConfig, SessionConfig
config = AdminConfig()
# Configure Redis if URL provided
redis_config = None
if config.REDIS_URL:
redis_config = RedisConfig(url=config.REDIS_URL)
crud_admin = CRUDAdmin(
session=get_session,
sessions=SessionConfig(backend=config.SESSION_BACKEND, redis=redis_config),
admin_db_url=config.DATABASE_URL,
SECRET_KEY=config.SECRET_KEY,
)
# Register models...
return crud_admin.app
Security Patterns¶
Production Security Configuration¶
Comprehensive security setup for production environments using built-in CRUDAdmin security features.
Built-in IP Restrictions¶
CRUDAdmin provides built-in IP restriction functionality:
import os
from crudauth.ratelimit import LockoutConfig
from crudadmin import AccessConfig, CRUDAdmin, RedisConfig, SessionConfig
crud_admin = CRUDAdmin(
session=get_session,
SECRET_KEY=os.environ["ADMIN_SECRET_KEY"],
sessions=SessionConfig(
backend="redis",
redis=RedisConfig(url=os.environ["REDIS_URL"]),
timeout_minutes=60,
max_per_admin=5,
secure_cookies=True,
),
access=AccessConfig(
allowed_ips=["127.0.0.1", "192.168.1.100"],
allowed_networks=["192.168.1.0/24", "10.0.0.0/8"],
trusted_proxy_hops=1,
lockout=LockoutConfig(max_attempts=5, lockout_max_seconds=5 * 60),
enforce_https=True,
),
track_events=True,
)
allowed_ips/allowed_networkskeep everyone else away from the admin, login page included. Since anyone who can reach the login page can lock out an admin by guessing wrong on purpose, this is the main defense against that.trusted_proxy_hopsis the number of reverse proxies in front of the app. With it, the allowlist and the lockout see the real client IP fromX-Forwarded-For; leave it at0when the app is exposed directly, so the header can't be forged.lockoutshown here matches crudadmin's defaults: five failures lock the username and IP, starting at one minute and capped at five.- Changes need the session's CSRF token in an
X-CSRF-Tokenheader, which the interface sends for you; creating or editing an admin account also asks for the admin's password again.
Next Steps¶
Master these common patterns to build robust admin interfaces. For more advanced features and configurations, see the Advanced Topics section.
These patterns provide the foundation for most real-world CRUDAdmin implementations. Combine them based on your specific requirements to create powerful, efficient admin interfaces.