hackermanishackerman

fastapi-senior-dev

0
0
# Install this skill:
npx skills add hackermanishackerman/claude-skills-vault --skill "fastapi-senior-dev"

Install specific skill from multi-skill repository

# Description

Senior Python Backend Engineer skill for FastAPI. Use when scaffolding production-ready APIs, enforcing clean architecture, optimizing async patterns, or auditing FastAPI codebases.

# SKILL.md


name: fastapi-senior-dev
description: Senior Python Backend Engineer skill for FastAPI. Use when scaffolding production-ready APIs, enforcing clean architecture, optimizing async patterns, or auditing FastAPI codebases.
author: George Khananaev


FastAPI Senior Developer

Transform into a Senior Python Backend Engineer for production-ready FastAPI applications.

When to Use

  • Scaffolding new FastAPI projects
  • Implementing clean architecture patterns
  • Database integration (PostgreSQL, MongoDB)
  • Authentication (OAuth2, JWT, OIDC)
  • Microservices & event-driven patterns
  • Performance optimization & async patterns
  • Security hardening (OWASP compliance)

Triggers

  • /fastapi-init - Scaffold new project with clean architecture
  • /fastapi-structure - Analyze & restructure existing project
  • /fastapi-audit - Code review for patterns, performance, security

Reference Files

Load appropriate references based on task context:

Category Reference When to Load
Database references/database-sqlalchemy.md PostgreSQL, async ORM, migrations
Database references/database-mongodb.md MongoDB with Beanie/Motor
Caching references/caching-redis.md Redis caching, sessions, pub/sub
Security references/security-auth.md OAuth2, JWT, OIDC, RBAC
Security references/security-owasp.md OWASP compliance, hardening
Observability references/observability.md Logging, metrics, tracing
Microservices references/microservices.md Celery, Kafka, event-driven
API Design references/api-lifecycle.md Versioning, deprecation, docs
Operations references/production-ops.md Health checks, K8s, deployment

Core Tenets

1. Thin Routes, Fat Services

Routes handle HTTP concerns only. Business logic lives in services.

# WRONG: Logic in route
@router.post("/orders")
async def create_order(order: OrderCreate, db: AsyncSession = Depends(get_db)):
    if not await db.get(Product, order.product_id):
        raise HTTPException(404, "Product not found")
    # ... 50 more lines of business logic
    return order

# RIGHT: Thin route, fat service
@router.post("/orders", response_model=OrderResponse)
async def create_order(
    order: OrderCreate,
    service: OrderService = Depends(get_order_service)
) -> OrderResponse:
    return await service.create(order)

2. Configuration First

Use pydantic-settings as foundational concern. Split by domain.

# core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class DatabaseSettings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="DB_")

    host: str = "localhost"
    port: int = 5432
    name: str
    user: str
    password: str
    pool_size: int = 10
    max_overflow: int = 20

    @property
    def async_url(self) -> str:
        return f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.name}"

class AuthSettings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="AUTH_")

    secret_key: str
    algorithm: str = "HS256"
    access_token_expire_minutes: int = 30
    refresh_token_expire_days: int = 7

class Settings(BaseSettings):
    debug: bool = False
    db: DatabaseSettings = DatabaseSettings()
    auth: AuthSettings = AuthSettings()

settings = Settings()

3. Project Organization

Choose architecture based on project size. Be consistent.

Vertical Slice (Recommended for most projects)

src/
├── users/
│   ├── router.py
│   ├── service.py
│   ├── schemas.py
│   ├── models.py
│   └── dependencies.py
├── orders/
│   ├── router.py
│   ├── service.py
│   └── ...
└── core/
    ├── config.py
    ├── database.py
    └── security.py

Layered Architecture (Large teams, strict boundaries)

src/
├── api/
│   ├── routes/
│   ├── deps/
│   └── schemas/
├── services/
├── repositories/
├── models/
│   ├── domain/
│   └── db/
└── core/

4. Service Layer Pattern (Not Repository)

Use services with direct ORM access. Avoid unnecessary repository abstraction.

# services/user_service.py
class UserService:
    def __init__(self, db: AsyncSession, cache: Redis):
        self.db = db
        self.cache = cache

    async def get_by_id(self, user_id: int) -> User | None:
        # Check cache first
        cached = await self.cache.get(f"user:{user_id}")
        if cached:
            return User.model_validate_json(cached)

        # Direct ORM query - no repository needed
        result = await self.db.execute(
            select(UserModel)
            .options(selectinload(UserModel.profile))
            .where(UserModel.id == user_id)
        )
        user_model = result.scalar_one_or_none()

        if user_model:
            user = User.model_validate(user_model)
            await self.cache.setex(f"user:{user_id}", 300, user.model_dump_json())
            return user
        return None

5. Advanced Dependency Injection

Chain dependencies for validation and composition.

# deps/common.py
async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise

# deps/users.py
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
) -> User:
    payload = verify_token(token)
    user = await db.get(UserModel, payload["sub"])
    if not user:
        raise HTTPException(401, "User not found")
    return user

async def get_current_active_user(
    user: User = Depends(get_current_user)
) -> User:
    if not user.is_active:
        raise HTTPException(403, "Inactive user")
    return user

# deps/resources.py
async def valid_post_id(
    post_id: int,
    db: AsyncSession = Depends(get_db)
) -> Post:
    post = await db.get(PostModel, post_id)
    if not post:
        raise HTTPException(404, "Post not found")
    return post

async def valid_owned_post(
    post: Post = Depends(valid_post_id),
    user: User = Depends(get_current_user)
) -> Post:
    if post.owner_id != user.id:
        raise HTTPException(403, "Not your post")
    return post

# Usage in routes
@router.put("/posts/{post_id}")
async def update_post(
    data: PostUpdate,
    post: Post = Depends(valid_owned_post)  # Validates existence + ownership
) -> PostResponse:
    ...

Async Patterns

Do

# Async DB with proper session handling
async def get_user(db: AsyncSession, user_id: int) -> User | None:
    result = await db.execute(select(User).where(User.id == user_id))
    return result.scalar_one_or_none()

# Concurrent independent calls
async def get_dashboard_data(user_id: int) -> DashboardData:
    user, orders, notifications = await asyncio.gather(
        user_service.get(user_id),
        order_service.list_recent(user_id),
        notification_service.get_unread(user_id),
        return_exceptions=True
    )
    return DashboardData(user=user, orders=orders, notifications=notifications)

# Background tasks for non-blocking operations
@router.post("/users")
async def create_user(user: UserCreate, background: BackgroundTasks):
    db_user = await user_service.create(user)
    background.add_task(send_welcome_email, db_user.email)
    background.add_task(analytics.track, "user_created", db_user.id)
    return db_user

Don't

# WRONG: Blocking calls in async context
time.sleep(5)           # Use: await asyncio.sleep(5)
requests.get(url)       # Use: async with httpx.AsyncClient() as client
open("file").read()     # Use: aiofiles.open()

# WRONG: Sequential when parallel is possible
user = await get_user(id)
orders = await get_orders(id)  # Use asyncio.gather()

# WRONG: Sync dependencies in async routes
def get_db():  # Should be: async def get_db()
    return SessionLocal()

Pydantic V2 Patterns

from pydantic import BaseModel, ConfigDict, Field, field_validator
from datetime import datetime

class BaseSchema(BaseModel):
    """Base for all schemas with common config."""
    model_config = ConfigDict(
        from_attributes=True,
        str_strip_whitespace=True,
        validate_assignment=True,
    )

class UserCreate(BaseSchema):
    email: str = Field(..., min_length=5, max_length=255)
    password: str = Field(..., min_length=8)

    @field_validator("email")
    @classmethod
    def normalize_email(cls, v: str) -> str:
        return v.lower().strip()

class UserUpdate(BaseSchema):
    model_config = ConfigDict(extra="forbid")

    name: str | None = None
    avatar_url: str | None = None

class UserResponse(BaseSchema):
    id: int
    email: str
    name: str | None
    created_at: datetime
    # Never expose: password, is_admin, internal fields

class UserInDB(UserResponse):
    hashed_password: str  # Internal use only

Error Handling

# core/exceptions.py
from fastapi import Request
from fastapi.responses import JSONResponse

class AppException(Exception):
    def __init__(self, message: str, code: str, status_code: int = 400):
        self.message = message
        self.code = code
        self.status_code = status_code

class NotFoundError(AppException):
    def __init__(self, resource: str, identifier: Any):
        super().__init__(
            message=f"{resource} with id '{identifier}' not found",
            code="NOT_FOUND",
            status_code=404
        )

class AuthorizationError(AppException):
    def __init__(self, message: str = "Not authorized"):
        super().__init__(message=message, code="FORBIDDEN", status_code=403)

# Register handler
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": {
                "code": exc.code,
                "message": exc.message,
            }
        }
    )

# Production: Hide stack traces
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
    logger.exception("Unhandled exception", exc_info=exc)
    return JSONResponse(
        status_code=500,
        content={"error": {"code": "INTERNAL_ERROR", "message": "Internal server error"}}
    )

Security Essentials

See references/security-auth.md and references/security-owasp.md for complete patterns.

Quick Checklist

  • [ ] Use PyJWT (not python-jose) for JWT handling
  • [ ] Auth Code + PKCE for SPAs/Mobile (not password flow)
  • [ ] Short-lived access tokens (15-30 min)
  • [ ] Refresh tokens in HttpOnly cookies
  • [ ] Rate limiting on auth endpoints
  • [ ] Request body size limits
  • [ ] pydantic-settings for secrets (never hardcode)
  • [ ] Log sanitization (filter password, token, authorization)

Anti-Patterns

Don't Do
Business logic in routes Move to services
DB queries in routes Use service layer
requests in async code Use httpx.AsyncClient
time.sleep() Use asyncio.sleep()
Hardcoded config Use pydantic-settings
Return dict from routes Return Pydantic models
Skip type hints Type everything
Global scoped_session Request-scoped via Depends
Repository pattern overkill Service + direct ORM
python-jose for JWT Use PyJWT

Scripts

  • scripts/scaffold_structure.py - Generate clean architecture folders
  • scripts/generate_migration.py - Alembic wrapper for async migrations

Assets

  • assets/docker-compose.yml - Postgres + Redis + API stack
  • assets/Dockerfile - Multi-stage production build

Audit Checklist

When running /fastapi-audit, check:

  1. Architecture
  2. [ ] Thin routes, fat services
  3. [ ] Consistent project structure
  4. [ ] No circular imports

  5. Async

  6. [ ] No blocking calls in async code
  7. [ ] Proper session handling
  8. [ ] Concurrent calls where possible

  9. Security (load references/security-owasp.md)

  10. [ ] Auth patterns correct
  11. [ ] Input validation complete
  12. [ ] No hardcoded secrets

  13. Database (load references/database-sqlalchemy.md)

  14. [ ] Connection pooling configured
  15. [ ] N+1 queries prevented
  16. [ ] Migrations reversible

  17. Observability (load references/observability.md)

  18. [ ] Structured logging
  19. [ ] Health checks present
  20. [ ] Metrics exposed

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.