Claude Skill

python-fastapi-ops

FastAPI web framework patterns. Triggers on: fastapi, api endpoint, dependency injection, pydantic model, openapi, swagger, starlette, async api, rest api, uvicorn.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_python-fastapi-ops-3dfaf0b.zip · 17 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/python-fastapi-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

FastAPI Patterns

Modern async API development with FastAPI.

Basic Application

from fastapi import FastAPI
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan - startup and shutdown."""
    # Startup
    app.state.db = await create_db_pool()
    yield
    # Shutdown
    await app.state.db.close()

app = FastAPI(
    title="My API",
    version="1.0.0",
    lifespan=lifespan,
)

@app.get("/")
async def root():
    return {"message": "Hello World"}

Request/Response Models

from pydantic import BaseModel, Field, EmailStr
from datetime import datetime

class UserCreate(BaseModel):
    """Request model with validation."""
    name: str = Field(..., min_length=1, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)

class UserResponse(BaseModel):
    """Response model."""
    id: int
    name: str
    email: EmailStr
    created_at: datetime

    model_config = {"from_attributes": True}  # Enable ORM mode

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
    db_user = await create_user_in_db(user)
    return db_user

Path and Query Parameters

from fastapi import Query, Path
from typing import Annotated

@app.get("/users/{user_id}")
async def get_user(
    user_id: Annotated[int, Path(..., ge=1, description="User ID")],
):
    return await fetch_user(user_id)

@app.get("/users")
async def list_users(
    skip: Annotated[int, Query(ge=0)] = 0,
    limit: Annotated[int, Query(ge=1, le=100)] = 10,
    search: str | None = None,
):
    return await fetch_users(skip=skip, limit=limit, search=search)

Dependency Injection

from fastapi import Depends
from typing import Annotated

async def get_db():
    """Database session dependency."""
    async with async_session() as session:
        yield session

async def get_current_user(
    token: Annotated[str, Depends(oauth2_scheme)],
    db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
    """Authenticate and return current user."""
    user = await authenticate_token(db, token)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

# Annotated types for reuse
DB = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]

@app.get("/me")
async def get_me(user: CurrentUser):
    return user

Exception Handling

from fastapi import HTTPException
from fastapi.responses import JSONResponse

# Built-in HTTP exceptions
@app.get("/items/{item_id}")
async def get_item(item_id: int):
    item = await fetch_item(item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

# Custom exception handler
class ItemNotFoundError(Exception):
    def __init__(self, item_id: int):
        self.item_id = item_id

@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request, exc: ItemNotFoundError):
    return JSONResponse(
        status_code=404,
        content={"detail": f"Item {exc.item_id} not found"},
    )

Router Organization

from fastapi import APIRouter

# users.py
router = APIRouter(prefix="/users", tags=["users"])

@router.get("/")
async def list_users():
    return []

@router.get("/{user_id}")
async def get_user(user_id: int):
    return {"id": user_id}

# main.py
from app.routers import users, items

app.include_router(users.router)
app.include_router(items.router, prefix="/api/v1")

Quick Reference

Feature Usage
Path param @app.get("/items/{id}")
Query param def f(q: str = None)
Body def f(item: ItemCreate)
Dependency Depends(get_db)
Auth Depends(get_current_user)
Response model response_model=ItemResponse
Status code status_code=201

Additional Resources

  • ./references/dependency-injection.md - Advanced DI patterns, scopes, caching
  • ./references/middleware-patterns.md - Middleware chains, CORS, error handling
  • ./references/validation-serialization.md - Pydantic v2 patterns, custom validators
  • ./references/background-tasks.md - Background tasks, async workers, scheduling

Scripts

  • ./scripts/scaffold-api.sh - Generate API endpoint boilerplate

Assets

  • ./assets/fastapi-template.py - Production-ready FastAPI app skeleton

See Also

Prerequisites:

  • python-typing-ops - Pydantic models and type hints
  • python-async-ops - Async endpoint patterns

Related Skills:

  • python-database-ops - SQLAlchemy integration
  • python-observability-ops - Logging, metrics, tracing middleware
  • python-pytest-ops - API testing with TestClient
Files (claude-mods)
  • assets
    • fastapi-template.py 4.9 KB
      """
      Production-ready FastAPI application template.
      
      Usage:
          uvicorn main:app --reload  # Development
          uvicorn main:app --host 0.0.0.0 --port 8000  # Production
      """
      
      from contextlib import asynccontextmanager
      from typing import Annotated
      
      from fastapi import Depends, FastAPI, HTTPException, Request
      from fastapi.middleware.cors import CORSMiddleware
      from fastapi.responses import JSONResponse
      from pydantic import BaseModel, Field
      from pydantic_settings import BaseSettings
      
      
      # =============================================================================
      # Configuration
      # =============================================================================
      
      class Settings(BaseSettings):
          """Application settings from environment variables."""
      
          app_name: str = "My API"
          debug: bool = False
          database_url: str = "postgresql+asyncpg://user:pass@localhost/db"
          redis_url: str = "redis://localhost:6379/0"
          api_key: str = ""
      
          model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
      
      
      # Cache settings
      from functools import lru_cache
      
      @lru_cache
      def get_settings() -> Settings:
          return Settings()
      
      
      # =============================================================================
      # Lifespan Management
      # =============================================================================
      
      @asynccontextmanager
      async def lifespan(app: FastAPI):
          """Application startup and shutdown."""
          settings = get_settings()
      
          # Startup
          # app.state.db = await create_db_pool(settings.database_url)
          # app.state.redis = await create_redis_client(settings.redis_url)
          print(f"Starting {settings.app_name}...")
      
          yield
      
          # Shutdown
          # await app.state.db.close()
          # await app.state.redis.close()
          print("Shutting down...")
      
      
      # =============================================================================
      # Application
      # =============================================================================
      
      app = FastAPI(
          title="My API",
          version="1.0.0",
          lifespan=lifespan,
      )
      
      # CORS
      app.add_middleware(
          CORSMiddleware,
          allow_origins=["*"] if get_settings().debug else ["https://myapp.com"],
          allow_credentials=True,
          allow_methods=["*"],
          allow_headers=["*"],
      )
      
      
      # =============================================================================
      # Error Handling
      # =============================================================================
      
      @app.exception_handler(Exception)
      async def global_exception_handler(request: Request, exc: Exception):
          """Handle unhandled exceptions."""
          return JSONResponse(
              status_code=500,
              content={"detail": "Internal server error"},
          )
      
      
      # =============================================================================
      # Dependencies
      # =============================================================================
      
      async def get_db():
          """Database session dependency."""
          # async with async_session() as session:
          #     yield session
          yield None  # Placeholder
      
      
      DB = Annotated[None, Depends(get_db)]  # Replace None with actual type
      
      
      # =============================================================================
      # Models
      # =============================================================================
      
      class HealthResponse(BaseModel):
          status: str
          version: str
      
      
      class ItemCreate(BaseModel):
          name: str = Field(..., min_length=1, max_length=100)
          description: str | None = None
      
      
      class ItemResponse(BaseModel):
          id: int
          name: str
          description: str | None
      
          model_config = {"from_attributes": True}
      
      
      # =============================================================================
      # Routes
      # =============================================================================
      
      @app.get("/health", response_model=HealthResponse)
      async def health_check():
          """Health check endpoint."""
          return HealthResponse(status="healthy", version="1.0.0")
      
      
      @app.get("/items", response_model=list[ItemResponse])
      async def list_items(
          db: DB,
          skip: int = 0,
          limit: int = 10,
      ):
          """List all items."""
          # items = await db.execute(select(Item).offset(skip).limit(limit))
          # return items.scalars().all()
          return []
      
      
      @app.post("/items", response_model=ItemResponse, status_code=201)
      async def create_item(item: ItemCreate, db: DB):
          """Create a new item."""
          # db_item = Item(**item.model_dump())
          # db.add(db_item)
          # await db.commit()
          # await db.refresh(db_item)
          # return db_item
          return ItemResponse(id=1, name=item.name, description=item.description)
      
      
      @app.get("/items/{item_id}", response_model=ItemResponse)
      async def get_item(item_id: int, db: DB):
          """Get a single item."""
          # item = await db.get(Item, item_id)
          # if not item:
          #     raise HTTPException(status_code=404, detail="Item not found")
          # return item
          raise HTTPException(status_code=404, detail="Item not found")
      
      
      if __name__ == "__main__":
          import uvicorn
      
          uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
      
  • references
    • background-tasks.md 8.4 KB
      # FastAPI Background Tasks
      
      Async background processing patterns.
      
      ## Built-in BackgroundTasks
      
      ```python
      from fastapi import BackgroundTasks, FastAPI
      
      app = FastAPI()
      
      async def send_email(email: str, message: str):
          """Background task - runs after response sent."""
          # Simulate email sending
          await asyncio.sleep(2)
          print(f"Email sent to {email}: {message}")
      
      @app.post("/signup")
      async def signup(
          email: str,
          background_tasks: BackgroundTasks,
      ):
          # Create user synchronously
          user = create_user(email)
      
          # Queue background task
          background_tasks.add_task(send_email, email, "Welcome!")
      
          # Response sent immediately
          return {"message": "User created"}
      
      
      # Multiple tasks
      @app.post("/order")
      async def create_order(
          order: OrderCreate,
          background_tasks: BackgroundTasks,
      ):
          db_order = save_order(order)
      
          # Queue multiple tasks
          background_tasks.add_task(send_confirmation, order.email)
          background_tasks.add_task(update_inventory, order.items)
          background_tasks.add_task(notify_warehouse, db_order.id)
      
          return {"order_id": db_order.id}
      ```
      
      ## Dependency Injection with Background Tasks
      
      ```python
      from fastapi import Depends, BackgroundTasks
      from typing import Annotated
      
      async def audit_log(action: str, user_id: int):
          """Log user actions."""
          await db.execute(
              "INSERT INTO audit_log (action, user_id) VALUES ($1, $2)",
              action, user_id
          )
      
      def get_auditor(background_tasks: BackgroundTasks):
          """Factory for audit logging."""
          def log(action: str, user_id: int):
              background_tasks.add_task(audit_log, action, user_id)
          return log
      
      Auditor = Annotated[Callable, Depends(get_auditor)]
      
      @app.delete("/users/{user_id}")
      async def delete_user(
          user_id: int,
          current_user: CurrentUser,
          auditor: Auditor,
      ):
          await db.delete_user(user_id)
          auditor("user_deleted", current_user.id)
          return {"deleted": user_id}
      ```
      
      ## Longer Tasks with Celery
      
      ```python
      # tasks.py
      from celery import Celery
      
      celery_app = Celery(
          "tasks",
          broker="redis://localhost:6379/0",
          backend="redis://localhost:6379/0",
      )
      
      @celery_app.task
      def process_video(video_id: int):
          """Long-running task - handled by Celery worker."""
          video = get_video(video_id)
          processed = transcode(video)
          save_processed(processed)
          return {"status": "completed", "video_id": video_id}
      
      
      # api.py
      from fastapi import FastAPI
      from tasks import process_video
      
      app = FastAPI()
      
      @app.post("/videos/{video_id}/process")
      async def start_processing(video_id: int):
          # Queue task in Celery
          task = process_video.delay(video_id)
      
          return {
              "task_id": task.id,
              "status": "queued",
          }
      
      @app.get("/tasks/{task_id}")
      async def get_task_status(task_id: str):
          task = process_video.AsyncResult(task_id)
          return {
              "task_id": task_id,
              "status": task.status,
              "result": task.result if task.ready() else None,
          }
      ```
      
      ## Periodic Tasks with APScheduler
      
      ```python
      from apscheduler.schedulers.asyncio import AsyncIOScheduler
      from apscheduler.triggers.cron import CronTrigger
      from fastapi import FastAPI
      from contextlib import asynccontextmanager
      
      scheduler = AsyncIOScheduler()
      
      async def cleanup_expired_sessions():
          """Run daily at midnight."""
          await db.execute("DELETE FROM sessions WHERE expires < NOW()")
      
      async def send_daily_report():
          """Run daily at 9 AM."""
          report = await generate_report()
          await send_email("admin@example.com", report)
      
      @asynccontextmanager
      async def lifespan(app: FastAPI):
          # Startup - configure scheduler
          scheduler.add_job(
              cleanup_expired_sessions,
              CronTrigger(hour=0, minute=0),
              id="cleanup_sessions",
          )
          scheduler.add_job(
              send_daily_report,
              CronTrigger(hour=9, minute=0),
              id="daily_report",
          )
          scheduler.start()
      
          yield
      
          # Shutdown
          scheduler.shutdown()
      
      app = FastAPI(lifespan=lifespan)
      
      
      # Manual trigger endpoint (for testing)
      @app.post("/admin/trigger/{job_id}")
      async def trigger_job(job_id: str, admin: AdminUser):
          job = scheduler.get_job(job_id)
          if not job:
              raise HTTPException(status_code=404, detail="Job not found")
      
          job.modify(next_run_time=datetime.now())
          return {"message": f"Job {job_id} triggered"}
      ```
      
      ## Task Queues with Redis
      
      ```python
      import redis.asyncio as redis
      import json
      from uuid import uuid4
      
      class TaskQueue:
          def __init__(self, redis_url: str):
              self.redis = redis.from_url(redis_url)
              self.queue_name = "task_queue"
      
          async def enqueue(self, task_type: str, payload: dict) -> str:
              """Add task to queue."""
              task_id = str(uuid4())
              task = {
                  "id": task_id,
                  "type": task_type,
                  "payload": payload,
                  "status": "pending",
              }
              await self.redis.rpush(self.queue_name, json.dumps(task))
              await self.redis.set(f"task:{task_id}", json.dumps(task))
              return task_id
      
          async def get_status(self, task_id: str) -> dict | None:
              """Get task status."""
              data = await self.redis.get(f"task:{task_id}")
              return json.loads(data) if data else None
      
      
      # Worker (separate process)
      async def worker(queue: TaskQueue):
          """Process tasks from queue."""
          while True:
              task_data = await queue.redis.blpop(queue.queue_name, timeout=1)
              if not task_data:
                  continue
      
              task = json.loads(task_data[1])
              task["status"] = "processing"
              await queue.redis.set(f"task:{task['id']}", json.dumps(task))
      
              try:
                  result = await process_task(task)
                  task["status"] = "completed"
                  task["result"] = result
              except Exception as e:
                  task["status"] = "failed"
                  task["error"] = str(e)
      
              await queue.redis.set(f"task:{task['id']}", json.dumps(task))
      
      
      # API endpoints
      queue = TaskQueue("redis://localhost:6379")
      
      @app.post("/tasks")
      async def create_task(task_type: str, payload: dict):
          task_id = await queue.enqueue(task_type, payload)
          return {"task_id": task_id}
      
      @app.get("/tasks/{task_id}")
      async def get_task(task_id: str):
          task = await queue.get_status(task_id)
          if not task:
              raise HTTPException(status_code=404)
          return task
      ```
      
      ## Async Task Manager
      
      ```python
      import asyncio
      from contextlib import asynccontextmanager
      from typing import Callable, Awaitable
      
      class TaskManager:
          """Manage long-running async tasks."""
      
          def __init__(self):
              self._tasks: dict[str, asyncio.Task] = {}
              self._results: dict[str, Any] = {}
      
          async def start(self, task_id: str, coro: Awaitable):
              """Start a named task."""
              if task_id in self._tasks:
                  raise ValueError(f"Task {task_id} already running")
      
              async def wrapper():
                  try:
                      result = await coro
                      self._results[task_id] = {"status": "completed", "result": result}
                  except Exception as e:
                      self._results[task_id] = {"status": "failed", "error": str(e)}
                  finally:
                      self._tasks.pop(task_id, None)
      
              self._tasks[task_id] = asyncio.create_task(wrapper())
              return task_id
      
          def get_status(self, task_id: str) -> dict:
              if task_id in self._tasks:
                  return {"status": "running"}
              return self._results.get(task_id, {"status": "not_found"})
      
          async def cancel(self, task_id: str) -> bool:
              task = self._tasks.get(task_id)
              if task:
                  task.cancel()
                  return True
              return False
      
      
      # Global task manager
      task_manager = TaskManager()
      
      @app.post("/process")
      async def start_processing(data: ProcessRequest):
          task_id = str(uuid4())
          await task_manager.start(task_id, heavy_processing(data))
          return {"task_id": task_id}
      
      @app.get("/process/{task_id}")
      async def get_processing_status(task_id: str):
          return task_manager.get_status(task_id)
      ```
      
      ## Quick Reference
      
      | Method | Use Case | Runs Where |
      |--------|----------|------------|
      | `BackgroundTasks` | Quick async tasks | Same process |
      | Celery | Heavy processing | Worker process |
      | APScheduler | Periodic jobs | Same process |
      | Redis queue | Distributed tasks | Worker process |
      | `asyncio.Task` | In-memory async | Same process |
      
      | Pattern | Best For |
      |---------|----------|
      | BackgroundTasks | Email, webhooks, logging |
      | Celery | Video processing, ML, reports |
      | APScheduler | Cleanup, reports, sync |
      | Redis queue | Scalable task distribution |
      
    • dependency-injection.md 7.2 KB
      # FastAPI Dependency Injection Patterns
      
      Advanced patterns for managing dependencies in FastAPI.
      
      ## Basic Dependencies
      
      ```python
      from fastapi import Depends, FastAPI
      from typing import Annotated
      
      app = FastAPI()
      
      # Simple dependency
      async def get_db():
          db = DatabaseSession()
          try:
              yield db
          finally:
              await db.close()
      
      # Use with Annotated for reusability
      DB = Annotated[DatabaseSession, Depends(get_db)]
      
      @app.get("/items")
      async def get_items(db: DB):
          return await db.fetch_all("SELECT * FROM items")
      ```
      
      ## Dependency Hierarchy
      
      ```python
      from fastapi import Depends, HTTPException, Header
      from typing import Annotated
      
      # Base dependency
      async def get_db():
          async with async_session() as session:
              yield session
      
      # Depends on get_db
      async def get_current_user(
          db: Annotated[AsyncSession, Depends(get_db)],
          token: Annotated[str, Header()],
      ) -> User:
          user = await db.execute(
              select(User).where(User.token == token)
          )
          if not user:
              raise HTTPException(status_code=401)
          return user.scalar_one()
      
      # Depends on get_current_user
      async def get_admin_user(
          user: Annotated[User, Depends(get_current_user)],
      ) -> User:
          if not user.is_admin:
              raise HTTPException(status_code=403)
          return user
      
      # Reusable annotated types
      DB = Annotated[AsyncSession, Depends(get_db)]
      CurrentUser = Annotated[User, Depends(get_current_user)]
      AdminUser = Annotated[User, Depends(get_admin_user)]
      
      @app.get("/admin/users")
      async def admin_list_users(admin: AdminUser, db: DB):
          return await db.execute(select(User)).scalars().all()
      ```
      
      ## Class-Based Dependencies
      
      ```python
      from dataclasses import dataclass
      from fastapi import Depends, Query
      from typing import Annotated
      
      @dataclass
      class Pagination:
          """Reusable pagination parameters."""
          skip: int = 0
          limit: int = 10
      
          def __init__(
              self,
              skip: Annotated[int, Query(ge=0)] = 0,
              limit: Annotated[int, Query(ge=1, le=100)] = 10,
          ):
              self.skip = skip
              self.limit = limit
      
      # Use as dependency
      @app.get("/items")
      async def list_items(pagination: Annotated[Pagination, Depends()]):
          return await fetch_items(
              skip=pagination.skip,
              limit=pagination.limit
          )
      
      
      # Class with injected dependencies
      class UserService:
          def __init__(self, db: Annotated[AsyncSession, Depends(get_db)]):
              self.db = db
      
          async def get_user(self, user_id: int) -> User | None:
              result = await self.db.execute(
                  select(User).where(User.id == user_id)
              )
              return result.scalar_one_or_none()
      
      @app.get("/users/{user_id}")
      async def get_user(
          user_id: int,
          service: Annotated[UserService, Depends()],
      ):
          user = await service.get_user(user_id)
          if not user:
              raise HTTPException(status_code=404)
          return user
      ```
      
      ## Cached Dependencies
      
      ```python
      from functools import lru_cache
      from pydantic_settings import BaseSettings
      
      class Settings(BaseSettings):
          database_url: str
          redis_url: str
          api_key: str
      
          model_config = {"env_file": ".env"}
      
      @lru_cache
      def get_settings() -> Settings:
          """Cached settings - loaded once."""
          return Settings()
      
      # Use in dependencies
      async def get_db(settings: Annotated[Settings, Depends(get_settings)]):
          engine = create_async_engine(settings.database_url)
          async with AsyncSession(engine) as session:
              yield session
      ```
      
      ## Request-Scoped State
      
      ```python
      from fastapi import Request
      from contextvars import ContextVar
      from uuid import uuid4
      
      # Context variable for request ID
      request_id_var: ContextVar[str] = ContextVar("request_id")
      
      @app.middleware("http")
      async def add_request_id(request: Request, call_next):
          request_id = str(uuid4())
          request_id_var.set(request_id)
          request.state.request_id = request_id
      
          response = await call_next(request)
          response.headers["X-Request-ID"] = request_id
          return response
      
      # Access in dependencies
      def get_request_id() -> str:
          return request_id_var.get()
      
      @app.get("/trace")
      async def trace_request(request_id: Annotated[str, Depends(get_request_id)]):
          return {"request_id": request_id}
      ```
      
      ## Dependency Overrides for Testing
      
      ```python
      from fastapi.testclient import TestClient
      
      # Production dependency
      async def get_db():
          async with async_session() as session:
              yield session
      
      # Test override
      async def get_test_db():
          async with test_session() as session:
              yield session
      
      def test_create_user():
          app.dependency_overrides[get_db] = get_test_db
      
          with TestClient(app) as client:
              response = client.post("/users", json={"name": "Test"})
              assert response.status_code == 201
      
          app.dependency_overrides.clear()
      
      
      # Context manager for cleaner tests
      from contextlib import contextmanager
      
      @contextmanager
      def override_dependency(original, replacement):
          app.dependency_overrides[original] = replacement
          try:
              yield
          finally:
              app.dependency_overrides.pop(original, None)
      
      def test_with_override():
          with override_dependency(get_db, get_test_db):
              # Test code here
              pass
      ```
      
      ## Parameterized Dependencies
      
      ```python
      from fastapi import Depends
      from typing import Callable
      
      def require_permission(permission: str):
          """Factory for permission-checking dependencies."""
          async def check_permission(
              user: Annotated[User, Depends(get_current_user)],
          ):
              if permission not in user.permissions:
                  raise HTTPException(
                      status_code=403,
                      detail=f"Missing permission: {permission}"
                  )
              return user
          return check_permission
      
      @app.delete("/items/{item_id}")
      async def delete_item(
          item_id: int,
          user: Annotated[User, Depends(require_permission("items:delete"))],
      ):
          return {"deleted": item_id}
      
      
      # Rate limiting factory
      def rate_limit(requests: int, window: int):
          """Create rate limit dependency."""
          async def check_rate(
              request: Request,
              redis: Annotated[Redis, Depends(get_redis)],
          ):
              key = f"rate:{request.client.host}"
              count = await redis.incr(key)
              if count == 1:
                  await redis.expire(key, window)
              if count > requests:
                  raise HTTPException(status_code=429, detail="Rate limited")
          return check_rate
      
      @app.get("/api/search")
      async def search(
          q: str,
          _: Annotated[None, Depends(rate_limit(requests=100, window=60))],
      ):
          return {"query": q}
      ```
      
      ## Sub-Application Dependencies
      
      ```python
      from fastapi import FastAPI, Depends
      
      # Shared dependency for sub-app
      async def get_api_key(x_api_key: Annotated[str, Header()]):
          if x_api_key != "secret":
              raise HTTPException(status_code=401)
          return x_api_key
      
      # Sub-application with its own dependencies
      api_v1 = FastAPI(dependencies=[Depends(get_api_key)])
      
      @api_v1.get("/items")
      async def list_items():
          return []
      
      # Mount on main app
      app = FastAPI()
      app.mount("/api/v1", api_v1)
      ```
      
      ## Quick Reference
      
      | Pattern | Use Case |
      |---------|----------|
      | `Annotated[T, Depends(f)]` | Reusable dependency type |
      | Class dependency | Group related params |
      | `@lru_cache` | Cache settings/config |
      | Dependency factory | Parameterized checks |
      | `dependency_overrides` | Testing isolation |
      | Hierarchy | Auth → User → Admin chain |
      | `ContextVar` | Request-scoped state |
      
    • middleware-patterns.md 8.8 KB
      # FastAPI Middleware Patterns
      
      Request/response processing, CORS, security, and error handling.
      
      ## Basic Middleware
      
      ```python
      from fastapi import FastAPI, Request
      from starlette.middleware.base import BaseHTTPMiddleware
      import time
      
      app = FastAPI()
      
      # Function-based middleware
      @app.middleware("http")
      async def add_timing_header(request: Request, call_next):
          start = time.perf_counter()
          response = await call_next(request)
          duration = time.perf_counter() - start
          response.headers["X-Process-Time"] = f"{duration:.4f}"
          return response
      
      
      # Class-based middleware
      class TimingMiddleware(BaseHTTPMiddleware):
          async def dispatch(self, request: Request, call_next):
              start = time.perf_counter()
              response = await call_next(request)
              duration = time.perf_counter() - start
              response.headers["X-Process-Time"] = f"{duration:.4f}"
              return response
      
      app.add_middleware(TimingMiddleware)
      ```
      
      ## CORS Configuration
      
      ```python
      from fastapi.middleware.cors import CORSMiddleware
      
      app.add_middleware(
          CORSMiddleware,
          allow_origins=[
              "http://localhost:3000",
              "https://myapp.com",
          ],
          allow_credentials=True,
          allow_methods=["*"],  # Or specific: ["GET", "POST"]
          allow_headers=["*"],
          expose_headers=["X-Request-ID"],
          max_age=600,  # Cache preflight for 10 minutes
      )
      
      # Development: allow all origins
      if settings.debug:
          app.add_middleware(
              CORSMiddleware,
              allow_origins=["*"],
              allow_credentials=True,
              allow_methods=["*"],
              allow_headers=["*"],
          )
      ```
      
      ## Security Headers
      
      ```python
      from starlette.middleware.base import BaseHTTPMiddleware
      
      class SecurityHeadersMiddleware(BaseHTTPMiddleware):
          async def dispatch(self, request: Request, call_next):
              response = await call_next(request)
      
              # Security headers
              response.headers["X-Content-Type-Options"] = "nosniff"
              response.headers["X-Frame-Options"] = "DENY"
              response.headers["X-XSS-Protection"] = "1; mode=block"
              response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
      
              # CSP - customize for your app
              response.headers["Content-Security-Policy"] = (
                  "default-src 'self'; "
                  "script-src 'self' 'unsafe-inline'; "
                  "style-src 'self' 'unsafe-inline'"
              )
      
              # HSTS (only in production with HTTPS)
              if not request.url.scheme == "http":
                  response.headers["Strict-Transport-Security"] = (
                      "max-age=31536000; includeSubDomains"
                  )
      
              return response
      
      app.add_middleware(SecurityHeadersMiddleware)
      ```
      
      ## Request ID Tracking
      
      ```python
      from uuid import uuid4
      from contextvars import ContextVar
      
      request_id_ctx: ContextVar[str] = ContextVar("request_id", default="")
      
      class RequestIDMiddleware(BaseHTTPMiddleware):
          async def dispatch(self, request: Request, call_next):
              # Use existing or generate new
              request_id = request.headers.get("X-Request-ID") or str(uuid4())
      
              # Store in context for logging
              request_id_ctx.set(request_id)
              request.state.request_id = request_id
      
              response = await call_next(request)
              response.headers["X-Request-ID"] = request_id
      
              return response
      
      app.add_middleware(RequestIDMiddleware)
      
      
      # Access in endpoints
      @app.get("/trace")
      async def trace(request: Request):
          return {"request_id": request.state.request_id}
      ```
      
      ## Logging Middleware
      
      ```python
      import logging
      import time
      from starlette.middleware.base import BaseHTTPMiddleware
      
      logger = logging.getLogger(__name__)
      
      class LoggingMiddleware(BaseHTTPMiddleware):
          async def dispatch(self, request: Request, call_next):
              start = time.perf_counter()
      
              # Log request
              logger.info(
                  "Request started",
                  extra={
                      "method": request.method,
                      "path": request.url.path,
                      "client": request.client.host if request.client else None,
                  }
              )
      
              response = await call_next(request)
      
              # Log response
              duration = time.perf_counter() - start
              logger.info(
                  "Request completed",
                  extra={
                      "method": request.method,
                      "path": request.url.path,
                      "status": response.status_code,
                      "duration": f"{duration:.3f}s",
                  }
              )
      
              return response
      
      app.add_middleware(LoggingMiddleware)
      ```
      
      ## Error Handling Middleware
      
      ```python
      from fastapi import Request
      from fastapi.responses import JSONResponse
      import traceback
      
      class ErrorHandlingMiddleware(BaseHTTPMiddleware):
          async def dispatch(self, request: Request, call_next):
              try:
                  return await call_next(request)
              except Exception as exc:
                  # Log the full traceback
                  logger.exception(
                      "Unhandled exception",
                      extra={
                          "path": request.url.path,
                          "method": request.method,
                          "traceback": traceback.format_exc(),
                      }
                  )
      
                  # Return generic error (hide details in production)
                  return JSONResponse(
                      status_code=500,
                      content={
                          "detail": "Internal server error",
                          "request_id": getattr(request.state, "request_id", None),
                      },
                  )
      
      app.add_middleware(ErrorHandlingMiddleware)
      ```
      
      ## Rate Limiting
      
      ```python
      from collections import defaultdict
      from datetime import datetime, timedelta
      import asyncio
      
      class RateLimitMiddleware(BaseHTTPMiddleware):
          def __init__(self, app, requests: int = 100, window: int = 60):
              super().__init__(app)
              self.requests = requests
              self.window = window
              self.clients: dict[str, list[datetime]] = defaultdict(list)
              self.lock = asyncio.Lock()
      
          async def dispatch(self, request: Request, call_next):
              client_ip = request.client.host if request.client else "unknown"
              now = datetime.now()
              window_start = now - timedelta(seconds=self.window)
      
              async with self.lock:
                  # Remove old requests
                  self.clients[client_ip] = [
                      t for t in self.clients[client_ip]
                      if t > window_start
                  ]
      
                  if len(self.clients[client_ip]) >= self.requests:
                      return JSONResponse(
                          status_code=429,
                          content={"detail": "Rate limit exceeded"},
                          headers={
                              "Retry-After": str(self.window),
                              "X-RateLimit-Limit": str(self.requests),
                              "X-RateLimit-Remaining": "0",
                          },
                      )
      
                  self.clients[client_ip].append(now)
                  remaining = self.requests - len(self.clients[client_ip])
      
              response = await call_next(request)
              response.headers["X-RateLimit-Limit"] = str(self.requests)
              response.headers["X-RateLimit-Remaining"] = str(remaining)
              return response
      
      app.add_middleware(RateLimitMiddleware, requests=100, window=60)
      ```
      
      ## GZip Compression
      
      ```python
      from fastapi.middleware.gzip import GZipMiddleware
      
      app.add_middleware(
          GZipMiddleware,
          minimum_size=1000,  # Only compress responses > 1KB
      )
      ```
      
      ## Trusted Host Validation
      
      ```python
      from fastapi.middleware.trustedhost import TrustedHostMiddleware
      
      app.add_middleware(
          TrustedHostMiddleware,
          allowed_hosts=["example.com", "*.example.com"],
      )
      ```
      
      ## Middleware Order
      
      ```python
      # Middleware executes in REVERSE order of addition
      # Last added = First to process request, Last to process response
      
      app = FastAPI()
      
      # 1. Error handling (outermost - catches all errors)
      app.add_middleware(ErrorHandlingMiddleware)
      
      # 2. Logging (log after error handling)
      app.add_middleware(LoggingMiddleware)
      
      # 3. Request ID (needed for logging)
      app.add_middleware(RequestIDMiddleware)
      
      # 4. Security (before business logic)
      app.add_middleware(SecurityHeadersMiddleware)
      
      # 5. CORS (needs to be early for preflight)
      app.add_middleware(CORSMiddleware, ...)
      
      # 6. GZip (compress final response)
      app.add_middleware(GZipMiddleware, minimum_size=1000)
      
      # Request flow: GZip → CORS → Security → RequestID → Logging → Error → App
      # Response flow: App → Error → Logging → RequestID → Security → CORS → GZip
      ```
      
      ## Quick Reference
      
      | Middleware | Purpose |
      |------------|---------|
      | `CORSMiddleware` | Cross-origin requests |
      | `GZipMiddleware` | Response compression |
      | `TrustedHostMiddleware` | Host validation |
      | `BaseHTTPMiddleware` | Custom middleware base |
      | `@app.middleware("http")` | Simple function middleware |
      
      | Order Position | Middleware Type |
      |----------------|-----------------|
      | First (outer) | Error handling |
      | Early | Logging, tracing |
      | Middle | Auth, rate limiting |
      | Late | CORS, compression |
      
    • validation-serialization.md 7.3 KB
      # Pydantic v2 Validation & Serialization
      
      Modern validation patterns for FastAPI with Pydantic v2.
      
      ## Basic Models
      
      ```python
      from pydantic import BaseModel, Field, EmailStr
      from datetime import datetime
      from typing import Annotated
      
      class UserCreate(BaseModel):
          """Request model with field validation."""
          name: str = Field(..., min_length=1, max_length=100)
          email: EmailStr
          age: int = Field(..., ge=0, le=150)
          bio: str | None = Field(default=None, max_length=500)
      
      class UserResponse(BaseModel):
          """Response model with ORM support."""
          id: int
          name: str
          email: EmailStr
          created_at: datetime
      
          model_config = {"from_attributes": True}
      ```
      
      ## Custom Validators
      
      ```python
      from pydantic import BaseModel, field_validator, model_validator
      from typing import Self
      
      class UserCreate(BaseModel):
          username: str
          password: str
          password_confirm: str
      
          @field_validator("username")
          @classmethod
          def validate_username(cls, v: str) -> str:
              """Validate single field."""
              if not v.isalnum():
                  raise ValueError("Username must be alphanumeric")
              return v.lower()
      
          @model_validator(mode="after")
          def validate_passwords(self) -> Self:
              """Validate across multiple fields."""
              if self.password != self.password_confirm:
                  raise ValueError("Passwords don't match")
              return self
      
      
      # Before validation (raw input)
      class Config(BaseModel):
          port: int
      
          @field_validator("port", mode="before")
          @classmethod
          def parse_port(cls, v):
              """Convert string to int before validation."""
              if isinstance(v, str):
                  return int(v)
              return v
      ```
      
      ## Computed Fields
      
      ```python
      from pydantic import BaseModel, computed_field
      from datetime import datetime
      
      class User(BaseModel):
          first_name: str
          last_name: str
          birth_date: datetime
      
          @computed_field
          @property
          def full_name(self) -> str:
              return f"{self.first_name} {self.last_name}"
      
          @computed_field
          @property
          def age(self) -> int:
              today = datetime.now()
              return today.year - self.birth_date.year
      ```
      
      ## Field Serialization
      
      ```python
      from pydantic import BaseModel, field_serializer
      from datetime import datetime
      from decimal import Decimal
      
      class Order(BaseModel):
          id: int
          total: Decimal
          created_at: datetime
      
          @field_serializer("total")
          def serialize_total(self, value: Decimal) -> str:
              """Serialize Decimal as formatted string."""
              return f"${value:.2f}"
      
          @field_serializer("created_at")
          def serialize_date(self, value: datetime) -> str:
              """Serialize datetime as ISO string."""
              return value.isoformat()
      
      
      # Or use Annotated with serialization
      from pydantic import PlainSerializer
      
      FormattedDecimal = Annotated[
          Decimal,
          PlainSerializer(lambda v: f"${v:.2f}", return_type=str)
      ]
      
      class Order(BaseModel):
          total: FormattedDecimal
      ```
      
      ## Custom Types
      
      ```python
      from pydantic import BaseModel, GetCoreSchemaHandler
      from pydantic_core import CoreSchema, core_schema
      from typing import Any
      
      class PhoneNumber(str):
          """Custom phone number type with validation."""
      
          @classmethod
          def __get_pydantic_core_schema__(
              cls, source_type: Any, handler: GetCoreSchemaHandler
          ) -> CoreSchema:
              return core_schema.no_info_after_validator_function(
                  cls._validate,
                  core_schema.str_schema(),
              )
      
          @classmethod
          def _validate(cls, v: str) -> "PhoneNumber":
              # Remove non-digits
              digits = "".join(c for c in v if c.isdigit())
              if len(digits) != 10:
                  raise ValueError("Phone must be 10 digits")
              return cls(f"({digits[:3]}) {digits[3:6]}-{digits[6:]}")
      
      
      class Contact(BaseModel):
          name: str
          phone: PhoneNumber
      
      # Usage
      contact = Contact(name="John", phone="1234567890")
      print(contact.phone)  # (123) 456-7890
      ```
      
      ## Nested Models
      
      ```python
      from pydantic import BaseModel
      from datetime import datetime
      
      class Address(BaseModel):
          street: str
          city: str
          country: str = "USA"
      
      class Company(BaseModel):
          name: str
          address: Address
      
      class UserResponse(BaseModel):
          id: int
          name: str
          company: Company | None = None
          addresses: list[Address] = []
      
          model_config = {"from_attributes": True}
      ```
      
      ## Discriminated Unions
      
      ```python
      from pydantic import BaseModel, Field
      from typing import Literal, Union
      from typing_extensions import Annotated
      
      class Dog(BaseModel):
          pet_type: Literal["dog"]
          name: str
          breed: str
      
      class Cat(BaseModel):
          pet_type: Literal["cat"]
          name: str
          indoor: bool = True
      
      # Use discriminator for efficient parsing
      Pet = Annotated[
          Union[Dog, Cat],
          Field(discriminator="pet_type")
      ]
      
      class Owner(BaseModel):
          name: str
          pets: list[Pet]
      
      # FastAPI automatically validates
      @app.post("/owners")
      async def create_owner(owner: Owner):
          return owner
      ```
      
      ## Model Inheritance
      
      ```python
      from pydantic import BaseModel
      from datetime import datetime
      
      class BaseResponse(BaseModel):
          """Base for all responses."""
          model_config = {"from_attributes": True}
      
      class TimestampMixin(BaseModel):
          """Mixin for timestamp fields."""
          created_at: datetime
          updated_at: datetime
      
      class UserBase(BaseModel):
          name: str
          email: str
      
      class UserCreate(UserBase):
          password: str
      
      class UserResponse(UserBase, TimestampMixin, BaseResponse):
          id: int
      ```
      
      ## Partial Updates (PATCH)
      
      ```python
      from pydantic import BaseModel
      from typing import Any
      
      class UserUpdate(BaseModel):
          """All fields optional for partial updates."""
          name: str | None = None
          email: str | None = None
          bio: str | None = None
      
      @app.patch("/users/{user_id}")
      async def update_user(user_id: int, updates: UserUpdate):
          # Only get set fields
          update_data = updates.model_dump(exclude_unset=True)
      
          # Apply to existing user
          user = await get_user(user_id)
          for field, value in update_data.items():
              setattr(user, field, value)
      
          return user
      ```
      
      ## Validation Error Handling
      
      ```python
      from fastapi import Request
      from fastapi.responses import JSONResponse
      from fastapi.exceptions import RequestValidationError
      
      @app.exception_handler(RequestValidationError)
      async def validation_exception_handler(
          request: Request,
          exc: RequestValidationError
      ):
          """Custom validation error response."""
          errors = []
          for error in exc.errors():
              errors.append({
                  "field": ".".join(str(loc) for loc in error["loc"]),
                  "message": error["msg"],
                  "type": error["type"],
              })
      
          return JSONResponse(
              status_code=422,
              content={
                  "detail": "Validation error",
                  "errors": errors,
              },
          )
      ```
      
      ## Quick Reference
      
      | Feature | Pydantic v2 |
      |---------|-------------|
      | ORM mode | `model_config = {"from_attributes": True}` |
      | Field validator | `@field_validator("field")` |
      | Model validator | `@model_validator(mode="after")` |
      | Serializer | `@field_serializer("field")` |
      | Computed | `@computed_field` + `@property` |
      | Exclude unset | `model_dump(exclude_unset=True)` |
      | Discriminator | `Field(discriminator="type")` |
      
      | Validation | Usage |
      |------------|-------|
      | Required | `name: str` |
      | Optional | `name: str \| None = None` |
      | Default | `name: str = "default"` |
      | Constraints | `Field(min_length=1, max_length=100)` |
      | Custom | `@field_validator` |
      
  • scripts
    • scaffold-api.sh 5.4 KB
      #!/usr/bin/env bash
      # Generate FastAPI endpoint boilerplate (Pydantic models + CRUD router) to stdout.
      #
      # Usage:   scaffold-api.sh <resource_name>
      # Input:   one positional arg — a singular resource name (e.g. "user", "order")
      # Output:  a complete FastAPI module (models + APIRouter CRUD endpoints) on stdout;
      #          redirect into a file to use it (the script emits to stdout and never
      #          opens a file itself, so it cannot clobber a user's project)
      # Stderr:  usage and error messages only
      # Exit:    0 ok, 2 usage (missing resource / unknown flag)
      #
      # Examples:
      #   scaffold-api.sh user > routers/user.py
      #   scaffold-api.sh order | tee routers/orders.py
      
      set -uo pipefail
      
      usage() {
        cat <<'EOF'
      Usage: scaffold-api.sh <resource_name>
      
      Generate FastAPI endpoint boilerplate — Pydantic Create/Update/Response models
      plus an APIRouter with list/create/get/update/delete CRUD endpoints — printed to
      stdout. Redirect into a module file to use it.
      
      Arguments:
        resource_name   a singular resource name, e.g. "user" or "order"
      
      Exit codes: 0 ok, 2 usage (missing resource / unknown flag).
      
      Examples:
        scaffold-api.sh user > routers/user.py
        scaffold-api.sh order | tee routers/orders.py
      EOF
      }
      
      # Flags first: --help short-circuits; any unknown flag is a hard usage error.
      while [[ $# -gt 0 ]]; do
        case "$1" in
          -h|--help) usage; exit 0 ;;
          --) shift; break ;;
          -*) echo "scaffold-api.sh: unknown option: $1" >&2; usage >&2; exit 2 ;;
          *) break ;;
        esac
      done
      
      RESOURCE="${1:-}"
      
      if [[ -z "$RESOURCE" ]]; then
        echo "scaffold-api.sh: resource name required" >&2
        usage >&2
        exit 2
      fi
      
      # Convert to different cases
      RESOURCE_LOWER=$(echo "$RESOURCE" | tr '[:upper:]' '[:lower:]')
      RESOURCE_UPPER=$(echo "$RESOURCE" | tr '[:lower:]' '[:upper:]')
      RESOURCE_TITLE=$(echo "$RESOURCE_LOWER" | sed 's/\b\(.\)/\u\1/g')
      RESOURCE_PLURAL="${RESOURCE_LOWER}s"
      
      cat << EOF
      # =============================================================================
      # ${RESOURCE_TITLE} Models
      # =============================================================================
      
      from pydantic import BaseModel, Field
      from datetime import datetime
      
      class ${RESOURCE_TITLE}Create(BaseModel):
          """Create ${RESOURCE_LOWER} request."""
          name: str = Field(..., min_length=1, max_length=100)
          # Add more fields
      
      class ${RESOURCE_TITLE}Update(BaseModel):
          """Update ${RESOURCE_LOWER} request (partial)."""
          name: str | None = None
          # Add more fields
      
      class ${RESOURCE_TITLE}Response(BaseModel):
          """${RESOURCE_TITLE} response."""
          id: int
          name: str
          created_at: datetime
          updated_at: datetime
      
          model_config = {"from_attributes": True}
      
      
      # =============================================================================
      # ${RESOURCE_TITLE} Router
      # =============================================================================
      
      from fastapi import APIRouter, Depends, HTTPException
      from typing import Annotated
      
      router = APIRouter(prefix="/${RESOURCE_PLURAL}", tags=["${RESOURCE_PLURAL}"])
      
      @router.get("/", response_model=list[${RESOURCE_TITLE}Response])
      async def list_${RESOURCE_PLURAL}(
          db: DB,
          skip: int = 0,
          limit: int = 10,
      ):
          """List all ${RESOURCE_PLURAL}."""
          result = await db.execute(
              select(${RESOURCE_TITLE}).offset(skip).limit(limit)
          )
          return result.scalars().all()
      
      @router.post("/", response_model=${RESOURCE_TITLE}Response, status_code=201)
      async def create_${RESOURCE_LOWER}(data: ${RESOURCE_TITLE}Create, db: DB):
          """Create a new ${RESOURCE_LOWER}."""
          ${RESOURCE_LOWER} = ${RESOURCE_TITLE}(**data.model_dump())
          db.add(${RESOURCE_LOWER})
          await db.commit()
          await db.refresh(${RESOURCE_LOWER})
          return ${RESOURCE_LOWER}
      
      @router.get("/{${RESOURCE_LOWER}_id}", response_model=${RESOURCE_TITLE}Response)
      async def get_${RESOURCE_LOWER}(${RESOURCE_LOWER}_id: int, db: DB):
          """Get a ${RESOURCE_LOWER} by ID."""
          ${RESOURCE_LOWER} = await db.get(${RESOURCE_TITLE}, ${RESOURCE_LOWER}_id)
          if not ${RESOURCE_LOWER}:
              raise HTTPException(status_code=404, detail="${RESOURCE_TITLE} not found")
          return ${RESOURCE_LOWER}
      
      @router.patch("/{${RESOURCE_LOWER}_id}", response_model=${RESOURCE_TITLE}Response)
      async def update_${RESOURCE_LOWER}(
          ${RESOURCE_LOWER}_id: int,
          data: ${RESOURCE_TITLE}Update,
          db: DB,
      ):
          """Update a ${RESOURCE_LOWER}."""
          ${RESOURCE_LOWER} = await db.get(${RESOURCE_TITLE}, ${RESOURCE_LOWER}_id)
          if not ${RESOURCE_LOWER}:
              raise HTTPException(status_code=404, detail="${RESOURCE_TITLE} not found")
      
          for field, value in data.model_dump(exclude_unset=True).items():
              setattr(${RESOURCE_LOWER}, field, value)
      
          await db.commit()
          await db.refresh(${RESOURCE_LOWER})
          return ${RESOURCE_LOWER}
      
      @router.delete("/{${RESOURCE_LOWER}_id}", status_code=204)
      async def delete_${RESOURCE_LOWER}(${RESOURCE_LOWER}_id: int, db: DB):
          """Delete a ${RESOURCE_LOWER}."""
          ${RESOURCE_LOWER} = await db.get(${RESOURCE_TITLE}, ${RESOURCE_LOWER}_id)
          if not ${RESOURCE_LOWER}:
              raise HTTPException(status_code=404, detail="${RESOURCE_TITLE} not found")
      
          await db.delete(${RESOURCE_LOWER})
          await db.commit()
      
      # =============================================================================
      # Include in main app:
      # from routers.${RESOURCE_PLURAL} import router as ${RESOURCE_PLURAL}_router
      # app.include_router(${RESOURCE_PLURAL}_router, prefix="/api/v1")
      # =============================================================================
      EOF
      
  • tests
    • run.sh 5.1 KB
      #!/usr/bin/env bash
      # Self-test for python-fastapi-ops — fully offline, deterministic, Linux-safe.
      #
      # scaffold-api.sh emits a FastAPI module to stdout; it never opens a file for
      # writing, so it cannot clobber a user's project. Every run here is redirected
      # into a mktemp -d sandbox — nothing is ever written into the repo or a real
      # project. Asserts the protocol contract (--help, exit codes, stream
      # separation), the generated module's structure, byte-identical idempotency,
      # and that the script owns no overwrite hazard (the only clobber is the
      # caller's shell `>` redirect — a latent caller-side hazard, documented here,
      # NOT "fixed" in the script per its contract).
      #
      # Usage:   bash tests/run.sh
      # Exit:    0 all pass, 1 one or more failures
      
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      V="$SKILL/scripts/scaffold-api.sh"
      
      SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
      PASS=0; FAIL=0
      ok() { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1"; }
      no() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1"; }
      expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; }
      expect_has()  { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
      
      echo "=== python-fastapi-ops self-test ==="
      
      # ── contract ──────────────────────────────────────────────────────────────────
      echo "-- contract --"
      bash -n "$V" 2>/dev/null && ok "bash -n scaffold-api.sh" || no "bash -n scaffold-api.sh"
      bash "$V" --help >/dev/null 2>&1; expect_exit "--help exits 0" 0 $?
      bash "$V" -h     >/dev/null 2>&1; expect_exit "-h exits 0" 0 $?
      out="$(bash "$V" --help 2>/dev/null)"
      expect_has "--help has Examples" "xamples" "$out"
      expect_has "--help documents exit 2" "2" "$out"
      bash "$V"          >/dev/null 2>&1; expect_exit "missing resource -> 2" 2 $?
      bash "$V" --bogus  >/dev/null 2>&1; expect_exit "unknown flag -> 2" 2 $?
      
      # usage/errors must go to stderr, never polluting the stdout data stream
      err="$(bash "$V" 2>&1 >/dev/null)"
      expect_has "missing-resource usage on stderr" "Usage:" "$err"
      noso="$(bash "$V" --bogus 2>/dev/null)"
      [[ -z "$noso" ]] && ok "unknown-flag writes nothing to stdout" || no "unknown-flag leaked to stdout"
      
      # ── generated module structure ────────────────────────────────────────────────
      echo "-- generated module --"
      bash "$V" user >"$SB/user.py" 2>"$SB/user.err"; expect_exit "generate user -> 0" 0 $?
      [[ -s "$SB/user.py" ]] && ok "module written to redirected stdout" || no "module not written"
      [[ ! -s "$SB/user.err" ]] && ok "happy path is silent on stderr" || no "happy path wrote to stderr"
      out="$(cat "$SB/user.py")"
      expect_has "has Pydantic Create model"  "class UserCreate(BaseModel):" "$out"
      expect_has "has Pydantic Update model"  "class UserUpdate(BaseModel):" "$out"
      expect_has "has Pydantic Response model" "class UserResponse(BaseModel):" "$out"
      expect_has "has APIRouter with prefix"  'router = APIRouter(prefix="/users"' "$out"
      expect_has "has create endpoint"  "async def create_user" "$out"
      expect_has "has list endpoint"    "async def list_users" "$out"
      expect_has "has get endpoint"     "async def get_user" "$out"
      expect_has "has update endpoint"  "async def update_user" "$out"
      expect_has "has delete endpoint"  "async def delete_user" "$out"
      expect_has "resource name in Create docstring" "Create user request" "$out"
      
      # title-casing + pluralization for mixed-case input
      bash "$V" Order >"$SB/order.py" 2>/dev/null; expect_exit "generate Order -> 0" 0 $?
      out="$(cat "$SB/order.py")"
      expect_has "title-cases Order"        "class OrderCreate(BaseModel):" "$out"
      expect_has "pluralizes to orders"     'prefix="/orders"' "$out"
      expect_has "orders create endpoint"   "async def create_order" "$out"
      
      # ── idempotency: re-running produces byte-identical output ───────────────────
      echo "-- idempotency --"
      bash "$V" user >"$SB/a.py" 2>/dev/null
      bash "$V" user >"$SB/b.py" 2>/dev/null
      if cmp -s "$SB/a.py" "$SB/b.py"; then ok "re-run produces byte-identical output"; else no "re-run output differs"; fi
      
      # ── overwrite safety ─────────────────────────────────────────────────────────
      echo "-- overwrite safety --"
      # The script emits to stdout only and never opens a file, so it is
      # non-destructive by construction: running it leaves the cwd untouched. There
      # is no --force/--no-clobber flag because the script has no file target to
      # guard — the sole clobber path is the caller's own shell redirect (`>`),
      # which the script cannot see. That caller-side hazard is documented here,
      # not "fixed".
      mkdir -p "$SB/cwd-check"
      ( cd "$SB/cwd-check" && bash "$V" user >/dev/null 2>&1 )
      n="$(find "$SB/cwd-check" -type f | wc -l | tr -d ' ')"
      [[ "$n" == "0" ]] && ok "script creates no files in cwd (non-destructive)" || no "script wrote files into cwd ($n)"
      
      echo ""
      echo "=== $PASS passed, $FAIL failed ==="
      [[ "$FAIL" -eq 0 ]] || exit 1
      exit 0
      
  • SKILL.md 5.2 KB
    ---
    name: python-fastapi-ops
    description: "FastAPI web framework patterns. Triggers on: fastapi, api endpoint, dependency injection, pydantic model, openapi, swagger, starlette, async api, rest api, uvicorn."
    license: MIT
    compatibility: "FastAPI 0.100+, Pydantic v2, Python 3.10+. Requires uvicorn for production."
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      depends-on: python-typing-ops, python-async-ops
      related-skills: python-database-ops, python-observability-ops, python-pytest-ops
    ---
    
    # FastAPI Patterns
    
    Modern async API development with FastAPI.
    
    ## Basic Application
    
    ```python
    from fastapi import FastAPI
    from contextlib import asynccontextmanager
    
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        """Application lifespan - startup and shutdown."""
        # Startup
        app.state.db = await create_db_pool()
        yield
        # Shutdown
        await app.state.db.close()
    
    app = FastAPI(
        title="My API",
        version="1.0.0",
        lifespan=lifespan,
    )
    
    @app.get("/")
    async def root():
        return {"message": "Hello World"}
    ```
    
    ## Request/Response Models
    
    ```python
    from pydantic import BaseModel, Field, EmailStr
    from datetime import datetime
    
    class UserCreate(BaseModel):
        """Request model with validation."""
        name: str = Field(..., min_length=1, max_length=100)
        email: EmailStr
        age: int = Field(..., ge=0, le=150)
    
    class UserResponse(BaseModel):
        """Response model."""
        id: int
        name: str
        email: EmailStr
        created_at: datetime
    
        model_config = {"from_attributes": True}  # Enable ORM mode
    
    @app.post("/users", response_model=UserResponse, status_code=201)
    async def create_user(user: UserCreate):
        db_user = await create_user_in_db(user)
        return db_user
    ```
    
    ## Path and Query Parameters
    
    ```python
    from fastapi import Query, Path
    from typing import Annotated
    
    @app.get("/users/{user_id}")
    async def get_user(
        user_id: Annotated[int, Path(..., ge=1, description="User ID")],
    ):
        return await fetch_user(user_id)
    
    @app.get("/users")
    async def list_users(
        skip: Annotated[int, Query(ge=0)] = 0,
        limit: Annotated[int, Query(ge=1, le=100)] = 10,
        search: str | None = None,
    ):
        return await fetch_users(skip=skip, limit=limit, search=search)
    ```
    
    ## Dependency Injection
    
    ```python
    from fastapi import Depends
    from typing import Annotated
    
    async def get_db():
        """Database session dependency."""
        async with async_session() as session:
            yield session
    
    async def get_current_user(
        token: Annotated[str, Depends(oauth2_scheme)],
        db: Annotated[AsyncSession, Depends(get_db)],
    ) -> User:
        """Authenticate and return current user."""
        user = await authenticate_token(db, token)
        if not user:
            raise HTTPException(status_code=401, detail="Invalid token")
        return user
    
    # Annotated types for reuse
    DB = Annotated[AsyncSession, Depends(get_db)]
    CurrentUser = Annotated[User, Depends(get_current_user)]
    
    @app.get("/me")
    async def get_me(user: CurrentUser):
        return user
    ```
    
    ## Exception Handling
    
    ```python
    from fastapi import HTTPException
    from fastapi.responses import JSONResponse
    
    # Built-in HTTP exceptions
    @app.get("/items/{item_id}")
    async def get_item(item_id: int):
        item = await fetch_item(item_id)
        if not item:
            raise HTTPException(status_code=404, detail="Item not found")
        return item
    
    # Custom exception handler
    class ItemNotFoundError(Exception):
        def __init__(self, item_id: int):
            self.item_id = item_id
    
    @app.exception_handler(ItemNotFoundError)
    async def item_not_found_handler(request, exc: ItemNotFoundError):
        return JSONResponse(
            status_code=404,
            content={"detail": f"Item {exc.item_id} not found"},
        )
    ```
    
    ## Router Organization
    
    ```python
    from fastapi import APIRouter
    
    # users.py
    router = APIRouter(prefix="/users", tags=["users"])
    
    @router.get("/")
    async def list_users():
        return []
    
    @router.get("/{user_id}")
    async def get_user(user_id: int):
        return {"id": user_id}
    
    # main.py
    from app.routers import users, items
    
    app.include_router(users.router)
    app.include_router(items.router, prefix="/api/v1")
    ```
    
    ## Quick Reference
    
    | Feature | Usage |
    |---------|-------|
    | Path param | `@app.get("/items/{id}")` |
    | Query param | `def f(q: str = None)` |
    | Body | `def f(item: ItemCreate)` |
    | Dependency | `Depends(get_db)` |
    | Auth | `Depends(get_current_user)` |
    | Response model | `response_model=ItemResponse` |
    | Status code | `status_code=201` |
    
    ## Additional Resources
    
    - `./references/dependency-injection.md` - Advanced DI patterns, scopes, caching
    - `./references/middleware-patterns.md` - Middleware chains, CORS, error handling
    - `./references/validation-serialization.md` - Pydantic v2 patterns, custom validators
    - `./references/background-tasks.md` - Background tasks, async workers, scheduling
    
    ## Scripts
    
    - `./scripts/scaffold-api.sh` - Generate API endpoint boilerplate
    
    ## Assets
    
    - `./assets/fastapi-template.py` - Production-ready FastAPI app skeleton
    
    ---
    
    ## See Also
    
    **Prerequisites:**
    - `python-typing-ops` - Pydantic models and type hints
    - `python-async-ops` - Async endpoint patterns
    
    **Related Skills:**
    - `python-database-ops` - SQLAlchemy integration
    - `python-observability-ops` - Logging, metrics, tracing middleware
    - `python-pytest-ops` - API testing with TestClient
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related