home / skills / markus41 / claude / beanie-odm

This skill helps you design and implement Beanie ODM patterns for MongoDB with FastAPI, enabling async modeling, querying, and migrations.

npx playbooks add skill markus41/claude --skill beanie-odm

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.2 KB
---
name: Beanie ODM
description: This skill should be used when the user asks to "create MongoDB model", "define Beanie document", "write MongoDB query", "create aggregation pipeline", "run database migration", "index MongoDB collection", or mentions Beanie, Motor, MongoDB documents, or async database operations. Provides MongoDB/Beanie ODM patterns for FastAPI.
version: 0.1.0
---

# Beanie ODM for MongoDB

This skill provides patterns for MongoDB integration using Beanie ODM with async Motor driver, optimized for FastAPI applications.

## Database Initialization

### Connection Setup

```python
from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient
from app.domains.users.models import User
from app.domains.products.models import Product

async def init_database(settings: Settings):
    client = AsyncIOMotorClient(settings.mongodb_url)

    await init_beanie(
        database=client[settings.database_name],
        document_models=[
            User,
            Product,
            # Add all document models
        ]
    )
```

### Settings Configuration

```python
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    mongodb_url: str = "mongodb://localhost:27017"
    database_name: str = "app_db"

    class Config:
        env_file = ".env"
```

## Document Models

### Basic Document

```python
from beanie import Document, Indexed
from pydantic import Field, EmailStr
from datetime import datetime
from typing import Optional

class User(Document):
    email: Indexed(EmailStr, unique=True)
    name: str
    hashed_password: str
    is_active: bool = True
    created_at: datetime = Field(default_factory=datetime.utcnow)
    updated_at: datetime = Field(default_factory=datetime.utcnow)

    class Settings:
        name = "users"  # Collection name
        use_state_management = True

    class Config:
        json_schema_extra = {
            "example": {
                "email": "[email protected]",
                "name": "John Doe"
            }
        }
```

### Document with Relationships

```python
from beanie import Document, Link, BackLink
from typing import List, Optional

class Author(Document):
    name: str
    books: List[BackLink["Book"]] = Field(original_field="author")

    class Settings:
        name = "authors"

class Book(Document):
    title: str
    author: Link[Author]
    categories: List[Link["Category"]] = []

    class Settings:
        name = "books"

class Category(Document):
    name: str
    books: List[BackLink[Book]] = Field(original_field="categories")

    class Settings:
        name = "categories"
```

### Embedded Documents

```python
from beanie import Document
from pydantic import BaseModel
from typing import List

class Address(BaseModel):
    street: str
    city: str
    country: str
    postal_code: str

class Contact(BaseModel):
    type: str  # "email", "phone"
    value: str
    is_primary: bool = False

class Customer(Document):
    name: str
    addresses: List[Address] = []
    contacts: List[Contact] = []

    class Settings:
        name = "customers"
```

## Query Patterns

### Basic CRUD Operations

```python
# Create
user = User(email="[email protected]", name="John")
await user.insert()

# Create with validation
user = await User.insert_one(
    User(email="[email protected]", name="John")
)

# Read by ID
user = await User.get(user_id)

# Read with filter
users = await User.find(User.is_active == True).to_list()

# Update
user.name = "Jane"
await user.save()

# Partial update
await user.set({User.name: "Jane", User.updated_at: datetime.utcnow()})

# Delete
await user.delete()
```

### Advanced Queries

```python
from beanie.operators import In, RegEx, And, Or

# Find with operators
active_users = await User.find(
    And(
        User.is_active == True,
        User.created_at >= start_date
    )
).to_list()

# Regex search
users = await User.find(
    RegEx(User.name, "^John", options="i")
).to_list()

# In operator
users = await User.find(
    In(User.email, ["[email protected]", "[email protected]"])
).to_list()

# Pagination
users = await User.find_all().skip(20).limit(10).to_list()

# Sorting
users = await User.find_all().sort(-User.created_at).to_list()

# Projection (select specific fields)
users = await User.find_all().project(UserSummary).to_list()
```

## Aggregation Pipelines

```python
from beanie import PydanticObjectId

class UserStats(BaseModel):
    total_users: int
    active_users: int
    avg_age: float

# Aggregation pipeline
pipeline = [
    {"$match": {"is_active": True}},
    {"$group": {
        "_id": None,
        "total": {"$sum": 1},
        "avg_age": {"$avg": "$age"}
    }}
]

result = await User.aggregate(pipeline).to_list()

# Using Beanie aggregation
from beanie.odm.queries.aggregation import AggregationQuery

stats = await User.find(User.is_active == True).aggregate([
    {"$group": {
        "_id": "$department",
        "count": {"$sum": 1}
    }}
]).to_list()
```

## Indexes

```python
from beanie import Document, Indexed
from pymongo import IndexModel, ASCENDING, DESCENDING, TEXT

class Product(Document):
    # Single field index
    sku: Indexed(str, unique=True)

    # Compound index defined in Settings
    name: str
    category: str
    price: float
    description: str

    class Settings:
        name = "products"
        indexes = [
            # Compound index
            IndexModel(
                [("category", ASCENDING), ("price", DESCENDING)],
                name="category_price_idx"
            ),
            # Text index
            IndexModel(
                [("name", TEXT), ("description", TEXT)],
                name="search_idx"
            ),
            # TTL index
            IndexModel(
                [("expires_at", ASCENDING)],
                expireAfterSeconds=0,
                name="ttl_idx"
            )
        ]
```

## Transactions

```python
from beanie import Document
from motor.motor_asyncio import AsyncIOMotorClientSession

async def transfer_funds(
    from_account_id: str,
    to_account_id: str,
    amount: float,
    session: AsyncIOMotorClientSession
):
    async with await session.start_transaction():
        from_account = await Account.get(from_account_id, session=session)
        to_account = await Account.get(to_account_id, session=session)

        if from_account.balance < amount:
            raise ValueError("Insufficient funds")

        await from_account.set(
            {Account.balance: from_account.balance - amount},
            session=session
        )
        await to_account.set(
            {Account.balance: to_account.balance + amount},
            session=session
        )
```

## Service Layer Pattern

```python
from typing import List, Optional
from beanie import PydanticObjectId

class UserService:
    async def get_by_id(self, user_id: str) -> Optional[User]:
        return await User.get(PydanticObjectId(user_id))

    async def get_by_email(self, email: str) -> Optional[User]:
        return await User.find_one(User.email == email)

    async def get_all(
        self,
        skip: int = 0,
        limit: int = 100,
        is_active: Optional[bool] = None
    ) -> List[User]:
        query = User.find_all()
        if is_active is not None:
            query = User.find(User.is_active == is_active)
        return await query.skip(skip).limit(limit).to_list()

    async def create(self, data: UserCreate) -> User:
        user = User(**data.model_dump())
        await user.insert()
        return user

    async def update(self, user_id: str, data: UserUpdate) -> Optional[User]:
        user = await self.get_by_id(user_id)
        if not user:
            return None
        update_data = data.model_dump(exclude_unset=True)
        update_data["updated_at"] = datetime.utcnow()
        await user.set(update_data)
        return user
```

## Additional Resources

### Reference Files

For detailed patterns and migration guides:
- **`references/migrations.md`** - Database migration strategies
- **`references/performance.md`** - Query optimization tips
- **`references/relationships.md`** - Link and BackLink patterns

### Example Files

Working examples in `examples/`:
- **`examples/document_models.py`** - Complete document definitions
- **`examples/aggregations.py`** - Aggregation pipeline examples
- **`examples/service.py`** - Service layer implementation

Overview

This skill provides practical Beanie ODM patterns for MongoDB using the async Motor driver, tailored for FastAPI applications. It focuses on clear setups for connection initialization, document modeling (including links and embedded docs), query and aggregation patterns, indexing, transactions, and a service layer approach. Use it to standardize async data access and maintainable domain models in Python FastAPI projects.

How this skill works

The skill explains how to initialize Motor and register Beanie Document models, how to define Document classes with Indexes, Links, BackLinks and embedded Pydantic models, and how to perform async CRUD, advanced queries, aggregation pipelines, and transactions. It also describes defining indexes (including compound, text, TTL), and provides a service-layer pattern for encapsulating data access logic. Examples show idiomatic async operations and session-aware transactions.

When to use it

  • When you need to create or update MongoDB models for a FastAPI app using Beanie and Motor.
  • When you want async CRUD, pagination, filtering, sorting, or projection with Pydantic models.
  • When implementing relationships between documents (Link / BackLink) or embedded documents.
  • When building aggregation pipelines for analytics or reporting inside the ODM layer.
  • When defining indexes (unique, compound, text, TTL) and planning migration or performance improvements.

Best practices

  • Initialize the Motor client and call init_beanie once at app startup, registering all Document models.
  • Keep domain logic in a service layer that consumes Beanie models and returns Pydantic-friendly results.
  • Use Indexed fields and IndexModel definitions for unique constraints and query performance.
  • Prefer session-aware operations for multi-document updates and use transactions for critical updates.
  • Use projection and pagination on queries to limit payloads and improve performance.

Example use cases

  • Define a User document with unique email, timestamps, and implement create/read/update/delete in a UserService.
  • Model Author/Book/Category relations using Link and BackLink to drive fast, normalized lookups.
  • Build a search index on Product name and description using a text IndexModel and implement full-text search queries.
  • Run aggregation pipelines to compute department-level counts, averages, or custom user statistics.
  • Implement a funds transfer flow using a Motor session and a Beanie transaction to ensure atomic updates.

FAQ

How do I register multiple document models?

Pass all Document classes to init_beanie(document_models=[...]) during app startup using a Motor client and the target database.

When should I use Links vs embedded models?

Use Link/BackLink for normalized relationships and cross-collection references; use embedded models for small, denormalized data that belongs strictly to the parent document.