backend-dev

krazyuniks's avatarfrom krazyuniks

FastAPI backend development with SQLAlchemy 2.0, Pydantic v2, and async Python. Use for API endpoints, database models, migrations, authentication, and background tasks.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 4, 2026

When & Why to Use This Skill

This Claude skill facilitates high-performance backend development using a modern Python stack including FastAPI, SQLAlchemy 2.0, and Pydantic v2. It provides standardized, production-ready patterns for building asynchronous APIs, managing complex database schemas with Alembic, implementing OAuth2 authentication, and handling background processing with TaskIQ, all while enforcing a container-first execution model for consistency.

Use Cases

  • Rapidly bootstrapping a scalable FastAPI backend with a structured directory layout and pre-configured Docker environment.
  • Implementing asynchronous database operations and complex relationships using SQLAlchemy 2.0's modern declarative mapping.
  • Creating robust data validation and serialization layers with Pydantic v2 schemas to ensure API type safety.
  • Setting up secure user authentication and authorization flows using OAuth2, JWT, and FastAPI's dependency injection system.
  • Managing long-running background jobs and distributed task queues using TaskIQ with Redis integration.
  • Automating database migrations and schema versioning within containerized development and CI/CD pipelines.
namebackend-dev
descriptionFastAPI backend development with SQLAlchemy 2.0, Pydantic v2, and async Python. Use for API endpoints, database models, migrations, authentication, and background tasks.
contextfork

Backend Development Skill

Activation: FastAPI, SQLAlchemy, Pydantic, async Python, database operations

CRITICAL: Container-First Execution

NEVER run Python commands directly on the host. Always use Docker:

# WRONG - will fail
pytest tests/
ruff check app/

# RIGHT - use Docker
docker compose exec backend pytest tests/
docker compose exec backend ruff check app/

See .claude/rules/container-execution.md for full details.

Stack Overview

Component Technology Version Policy
Framework FastAPI Latest stable
ORM SQLAlchemy 2.0 Async with mapped_column
Validation Pydantic v2 BaseModel schemas
Migrations Alembic Async driver
Auth OAuth2 + JWT Tone 3000 integration
Queue TaskIQ Redis backend
Database PostgreSQL Async via asyncpg

Architecture Pattern

app/
├── api/           # Route handlers (thin layer)
│   ├── auth.py    # OAuth endpoints
│   ├── shootouts.py
│   ├── jobs.py
│   └── ws.py      # WebSocket handlers
├── core/          # Configuration & shared
│   ├── config.py  # Settings from env
│   ├── security.py
│   ├── database.py
│   └── tone3000.py # API client
├── models/        # SQLAlchemy models
├── schemas/       # Pydantic schemas
├── services/      # Business logic
└── tasks/         # Background jobs

Key Patterns

1. Route Handler (Thin Controller)

from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession

router = APIRouter(prefix="/shootouts", tags=["shootouts"])

@router.get("/{shootout_id}", response_model=ShootoutRead)
async def get_shootout(
    shootout_id: int,
    db: Annotated[AsyncSession, Depends(get_db)],
    current_user: Annotated[User, Depends(get_current_user)],
) -> Shootout:
    """Get a shootout by ID."""
    shootout = await db.get(Shootout, shootout_id)
    if not shootout or shootout.user_id != current_user.id:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Shootout not found"
        )
    return shootout

2. SQLAlchemy 2.0 Models

from sqlalchemy import ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    tone3000_id: Mapped[int] = mapped_column(unique=True, index=True)
    username: Mapped[str] = mapped_column(String(100))

    # Relationships
    shootouts: Mapped[list["Shootout"]] = relationship(
        back_populates="user",
        cascade="all, delete-orphan"
    )

class Shootout(Base):
    __tablename__ = "shootouts"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    title: Mapped[str] = mapped_column(String(200))
    config_json: Mapped[str] = mapped_column(Text)

    user: Mapped["User"] = relationship(back_populates="shootouts")

3. Pydantic Schemas

from pydantic import BaseModel, ConfigDict

class ShootoutBase(BaseModel):
    title: str
    config_json: str

class ShootoutCreate(ShootoutBase):
    pass

class ShootoutRead(ShootoutBase):
    model_config = ConfigDict(from_attributes=True)

    id: int
    user_id: int

4. Dependency Injection

from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import async_session_maker

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_maker() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise

async def get_current_user(
    token: Annotated[str, Depends(oauth2_scheme)],
    db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
    """Decode JWT and return current user."""
    payload = decode_token(token)
    user = await db.execute(
        select(User).where(User.tone3000_id == payload["sub"])
    )
    user = user.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

5. Background Tasks (TaskIQ)

from taskiq import TaskiqScheduler
from taskiq_redis import ListQueueBroker, RedisAsyncResultBackend

broker = ListQueueBroker(url="redis://redis:6379")
broker.with_result_backend(RedisAsyncResultBackend(redis_url="redis://redis:6379"))

@broker.task
async def process_shootout(job_id: str, shootout_id: int) -> str:
    """Process a shootout through the pipeline."""
    await update_job_status(job_id, "running", progress=0)

    # Process...
    await update_job_status(job_id, "running", progress=50)

    await update_job_status(job_id, "completed", progress=100)
    return output_path

Database Operations

Queries

from sqlalchemy import select
from sqlalchemy.orm import selectinload

# Get with eager loading
stmt = (
    select(Shootout)
    .where(Shootout.user_id == user_id)
    .options(selectinload(Shootout.jobs))
    .order_by(Shootout.created_at.desc())
)
result = await db.execute(stmt)
shootouts = result.scalars().all()

# Pagination
stmt = (
    select(Shootout)
    .offset(skip)
    .limit(limit)
)

Transactions

Service Layer Pattern: Services use begin_nested() when already in a transaction, otherwise begin():

# In services (gear_item_service.py, signal_chain_service.py, etc.)
context_manager = (
    session.begin_nested() if session.in_transaction() else session.begin()
)
async with context_manager:
    # ... do work, add(), flush() ...
# Savepoint released (if nested) or committed (if begin)

The Contract:

  • in_transaction()=False → Service owns commit via begin()
  • in_transaction()=True → Caller must commit (service uses savepoint)

Common Pitfall: Pre-check queries start implicit transactions:

# API endpoint
@router.post("/items")
async def create_item(db: DbSession, ...):
    # This query starts an implicit transaction!
    existing = await db.execute(select(Item).where(...))

    # Service sees in_transaction()=True, uses begin_nested()
    item = await service.create_item(...)

    # BUG: Without this, changes are never committed!
    await db.commit()  # ← REQUIRED when API queried before service call

    return item

Rules:

  1. If API does ANY query before calling a write service → must await db.commit() after
  2. If API only calls service (no pre-queries) → service handles commit via begin()
  3. Service-to-service calls → outer service owns commit, inner uses savepoint

Migrations (Alembic)

# Create migration
docker compose exec backend alembic revision --autogenerate -m "add jobs table"

# Apply migrations
docker compose exec backend alembic upgrade head

# Rollback
docker compose exec backend alembic downgrade -1

Error Handling

from fastapi import HTTPException, status

class AppError(Exception):
    """Base application error."""

class NotFoundError(AppError):
    """Resource not found."""

class UnauthorizedError(AppError):
    """User not authorized."""

# In routes
@router.get("/{id}")
async def get_item(id: int, db: AsyncSession = Depends(get_db)):
    item = await db.get(Item, id)
    if not item:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Item {id} not found"
        )
    return item

Quality Commands

# Lint
docker compose exec backend ruff check app/

# Type check
docker compose exec backend mypy app/

# Test
docker compose exec backend pytest tests/

# All checks
just check-backend

Resources

See resources/ for detailed guides:

  • tone3000-integration.md - OAuth and API patterns
  • taskiq-patterns.md - Background job patterns
  • websocket-progress.md - Real-time updates