strat-gameplay-webapp/backend/app/api/CLAUDE.md
Cal Corum 76e24ab22b CLAUDE: Refactor ManualOutcomeSubmission to use PlayOutcome enum + comprehensive documentation
## Refactoring
- Changed `ManualOutcomeSubmission.outcome` from `str` to `PlayOutcome` enum type
- Removed custom validator (Pydantic handles enum validation automatically)
- Added direct import of PlayOutcome (no circular dependency due to TYPE_CHECKING guard)
- Updated tests to use enum values while maintaining backward compatibility

Benefits:
- Better type safety with IDE autocomplete
- Cleaner code (removed 15 lines of validator boilerplate)
- Backward compatible (Pydantic auto-converts strings to enum)
- Access to helper methods (is_hit(), is_out(), etc.)

Files modified:
- app/models/game_models.py: Enum type + import
- tests/unit/config/test_result_charts.py: Updated 7 tests + added compatibility test

## Documentation
Created comprehensive CLAUDE.md files for all backend/app/ subdirectories to help future AI agents quickly understand and work with the code.

Added 8,799 lines of documentation covering:
- api/ (906 lines): FastAPI routes, health checks, auth patterns
- config/ (906 lines): League configs, PlayOutcome enum, result charts
- core/ (1,288 lines): GameEngine, StateManager, PlayResolver, dice system
- data/ (937 lines): API clients (planned), caching layer
- database/ (945 lines): Async sessions, operations, recovery
- models/ (1,270 lines): Pydantic/SQLAlchemy models, polymorphic patterns
- utils/ (959 lines): Logging, JWT auth, security
- websocket/ (1,588 lines): Socket.io handlers, real-time events
- tests/ (475 lines): Testing patterns and structure

Each CLAUDE.md includes:
- Purpose & architecture overview
- Key components with detailed explanations
- Patterns & conventions
- Integration points
- Common tasks (step-by-step guides)
- Troubleshooting with solutions
- Working code examples
- Testing guidance

Total changes: +9,294 lines / -24 lines
Tests: All passing (62/62 model tests, 7/7 ManualOutcomeSubmission tests)
2025-10-31 16:03:54 -05:00

23 KiB

Backend API Layer - REST Endpoints

Overview

The app/api/ directory contains REST API endpoints for the Paper Dynasty game backend. This layer provides traditional HTTP request/response endpoints for operations like authentication, game creation, health checks, and data retrieval.

Architecture Note: Most real-time gameplay happens via WebSocket (see app/websocket/). REST API is primarily for:

  • Authentication & authorization
  • Game lifecycle (create, list, retrieve)
  • Health monitoring
  • Data queries that don't require real-time updates

Directory Structure

app/api/
├── __init__.py           # Empty package marker
├── routes/               # API route modules
│   ├── __init__.py       # Empty package marker
│   ├── health.py         # Health check endpoints (✅ complete)
│   ├── auth.py           # Authentication endpoints (stub)
│   └── games.py          # Game CRUD endpoints (stub)
└── CLAUDE.md             # This file

Note: No dependencies.py file yet. Common dependencies (auth, DB sessions) will be added as needed during Phase 2+ implementation.

Current Implementation Status

Fully Implemented

Health Endpoints (routes/health.py)

Purpose: Service health monitoring for deployment and operations

Endpoints:

  1. GET /api/health - Application health check

    • Returns: Service status, timestamp, environment, version
    • Use: Load balancer health probes, monitoring systems
    • Response time: < 10ms
  2. GET /api/health/db - Database connectivity check

    • Returns: Database connection status
    • Use: Verify database availability
    • Response time: < 50ms
    • Graceful error handling (returns unhealthy status instead of 500)

Example Responses:

// GET /api/health
{
  "status": "healthy",
  "timestamp": "2025-10-31T14:23:45.123456+00:00",
  "environment": "development",
  "version": "1.0.0"
}

// GET /api/health/db (success)
{
  "status": "healthy",
  "database": "connected",
  "timestamp": "2025-10-31T14:23:45.123456+00:00"
}

// GET /api/health/db (failure)
{
  "status": "unhealthy",
  "database": "disconnected",
  "error": "connection refused",
  "timestamp": "2025-10-31T14:23:45.123456+00:00"
}

🟡 Stub Implementation (Phase 1)

Authentication Endpoints (routes/auth.py)

Purpose: User authentication via Discord OAuth (planned for Phase 1, currently stub)

Endpoints:

  1. POST /api/auth/token - Create JWT token

    • Request: TokenRequest (user_id, username, discord_id)
    • Response: TokenResponse (access_token, token_type)
    • Current: Directly creates token from provided data (no OAuth yet)
    • TODO: Implement full Discord OAuth flow
  2. GET /api/auth/verify - Verify authentication status

    • Response: Stub acknowledgment
    • TODO: Implement token verification with user context

TODO Items:

  • Discord OAuth callback handler
  • State parameter validation
  • Token refresh endpoint
  • User session management
  • Authentication middleware/dependency

Game Endpoints (routes/games.py)

Purpose: Game lifecycle management (planned for Phase 2+, currently stub)

Endpoints:

  1. GET /api/games/ - List all games

    • Response: List of GameListItem (game_id, league_id, status, teams)
    • TODO: Implement with database query, pagination, filters
  2. GET /api/games/{game_id} - Get game details

    • Response: Full game state and metadata
    • TODO: Load from database, include plays, lineups, current state
  3. POST /api/games/ - Create new game

    • Request: Game creation parameters (league, teams, settings)
    • Response: Created game details
    • TODO: Create game in DB, initialize state, return game_id

TODO Items:

  • Game creation with validation
  • Game listing with filters (by league, status, user)
  • Game detail retrieval with related data
  • Game deletion/cancellation
  • Game history and statistics

FastAPI Patterns Used

1. APIRouter Pattern

Each route module creates its own APIRouter instance:

from fastapi import APIRouter

router = APIRouter()

@router.get("/endpoint")
async def handler():
    ...

Routers are registered in app/main.py:

from app.api.routes import health, auth, games

app.include_router(health.router, prefix="/api", tags=["health"])
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(games.router, prefix="/api/games", tags=["games"])

Conventions:

  • Each module exports a router variable
  • Prefix and tags applied during registration in main.py
  • Tags group endpoints in Swagger UI docs

2. Pydantic Request/Response Models

Use Pydantic models for type safety and validation:

from pydantic import BaseModel

class TokenRequest(BaseModel):
    """Request model for token creation"""
    user_id: str
    username: str
    discord_id: str

class TokenResponse(BaseModel):
    """Response model for token creation"""
    access_token: str
    token_type: str = "bearer"

@router.post("/token", response_model=TokenResponse)
async def create_auth_token(request: TokenRequest):
    # FastAPI automatically validates request body against TokenRequest
    # and serializes return value according to TokenResponse
    ...

Benefits:

  • Automatic request validation with clear error messages
  • Automatic response serialization
  • OpenAPI schema generation for docs
  • Type hints for IDE support

3. Async Handlers

All route handlers use async def:

@router.get("/health/db")
async def database_health():
    # Can use await for DB queries, external APIs, etc.
    async with engine.connect() as conn:
        await conn.execute(text("SELECT 1"))
    ...

Why Async:

  • Non-blocking I/O operations (database, external APIs)
  • Better concurrency for high-traffic endpoints
  • Consistent with WebSocket handlers
  • Required for async database operations (asyncpg)

4. Logging Pattern

Module-level logger with descriptive name:

import logging

logger = logging.getLogger(f'{__name__}.health')

@router.get("/endpoint")
async def handler():
    logger.info("Endpoint called")
    logger.error(f"Error occurred: {error}", exc_info=True)

Logging Levels:

  • DEBUG: Detailed diagnostic information
  • INFO: Normal operation events (endpoint calls, state changes)
  • WARNING: Unusual but handled situations
  • ERROR: Error conditions that need attention
  • CRITICAL: Severe errors requiring immediate action

5. Error Handling

Use FastAPI's HTTPException for error responses:

from fastapi import HTTPException

@router.post("/endpoint")
async def handler():
    try:
        # operation
        return result
    except ValidationError as e:
        logger.error(f"Validation failed: {e}")
        raise HTTPException(status_code=400, detail="Invalid request data")
    except Exception as e:
        logger.error(f"Unexpected error: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail="Internal server error")

Status Codes:

  • 200: Success
  • 201: Created
  • 400: Bad request (validation error)
  • 401: Unauthorized (missing/invalid token)
  • 403: Forbidden (valid token, insufficient permissions)
  • 404: Not found
  • 500: Internal server error

6. Settings Injection

Use get_settings() for configuration:

from app.config import get_settings

settings = get_settings()

@router.get("/endpoint")
async def handler():
    api_url = settings.sba_api_url
    ...

Available Settings:

  • app_env: "development", "staging", "production"
  • debug: Enable debug mode
  • database_url: PostgreSQL connection string
  • secret_key: JWT signing key
  • discord_client_id, discord_client_secret: OAuth credentials
  • sba_api_url, pd_api_url: League API endpoints
  • cors_origins: Allowed frontend origins

Integration Points

With Other Backend Components

Database Layer (app/database/)

from app.database.session import get_session
from app.database.operations import DatabaseOperations

@router.get("/games/{game_id}")
async def get_game(game_id: str):
    db_ops = DatabaseOperations()
    game_data = await db_ops.load_game_state(game_id)
    return game_data

Models (app/models/)

from app.models import GameState
from app.models.db_models import Game

@router.post("/games/")
async def create_game(request: CreateGameRequest):
    # Use Pydantic models for validation
    # Use SQLAlchemy models for database operations
    ...

Authentication (app/utils/auth.py)

from app.utils.auth import create_token, verify_token

@router.post("/auth/token")
async def create_auth_token(request: TokenRequest):
    token = create_token(user_data)
    return TokenResponse(access_token=token)

State Manager (app/core/state_manager.py)

from app.core.state_manager import state_manager

@router.get("/games/{game_id}")
async def get_game(game_id: str):
    # Check in-memory state first (fast)
    state = state_manager.get_state(game_id)
    if not state:
        # Fall back to database
        state = await state_manager.recover_game(game_id)
    return state

With Frontend

Frontend applications make HTTP requests to these endpoints:

// Example frontend code (Vue/Nuxt)
const api = axios.create({
  baseURL: 'http://localhost:8000/api',
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

// Health check
const health = await api.get('/health');

// Create game
const game = await api.post('/games/', {
  league_id: 'sba',
  home_team_id: 1,
  away_team_id: 2
});

// List games
const games = await api.get('/games/');

Common Tasks

Adding a New Endpoint

  1. Choose appropriate route module (or create new one)

    # If creating new module
    touch app/api/routes/teams.py
    
  2. Define Pydantic models for request/response

    from pydantic import BaseModel
    
    class CreateTeamRequest(BaseModel):
        name: str
        league_id: str
        owner_id: str
    
    class TeamResponse(BaseModel):
        id: int
        name: str
        league_id: str
        created_at: str
    
  3. Create route handler

    import logging
    from fastapi import APIRouter, HTTPException
    from app.database.operations import DatabaseOperations
    
    logger = logging.getLogger(f'{__name__}.teams')
    router = APIRouter()
    
    @router.post("/", response_model=TeamResponse)
    async def create_team(request: CreateTeamRequest):
        """Create a new team"""
        logger.info(f"Creating team: {request.name}")
    
        try:
            db_ops = DatabaseOperations()
            team = await db_ops.create_team(
                name=request.name,
                league_id=request.league_id,
                owner_id=request.owner_id
            )
            return TeamResponse(**team)
        except Exception as e:
            logger.error(f"Failed to create team: {e}", exc_info=True)
            raise HTTPException(status_code=500, detail="Failed to create team")
    
  4. Register router in main.py

    from app.api.routes import teams
    
    app.include_router(teams.router, prefix="/api/teams", tags=["teams"])
    
  5. Test the endpoint

    # Start server
    python -m app.main
    
    # Test with curl
    curl -X POST http://localhost:8000/api/teams/ \
      -H "Content-Type: application/json" \
      -d '{"name": "Test Team", "league_id": "sba", "owner_id": "user123"}'
    
    # Or use Swagger UI
    # http://localhost:8000/docs
    

Adding Authentication Dependency

When authentication is implemented, add a dependency for protected endpoints:

from fastapi import Depends, HTTPException, Header
from app.utils.auth import verify_token

async def get_current_user(authorization: str = Header(...)):
    """Extract and verify user from JWT token"""
    try:
        scheme, token = authorization.split()
        if scheme.lower() != 'bearer':
            raise HTTPException(status_code=401, detail="Invalid auth scheme")

        payload = verify_token(token)
        return payload
    except Exception as e:
        raise HTTPException(status_code=401, detail="Invalid token")

@router.post("/games/")
async def create_game(
    request: CreateGameRequest,
    user = Depends(get_current_user)  # Automatically extracts and validates token
):
    # user contains decoded JWT payload
    owner_id = user['user_id']
    ...

Adding Database Operations

For endpoints that need database access:

from app.database.operations import DatabaseOperations
from app.database.session import get_session

@router.get("/games/{game_id}")
async def get_game(game_id: str):
    db_ops = DatabaseOperations()

    # Load game with all related data
    game_data = await db_ops.load_game_state(game_id)

    if not game_data:
        raise HTTPException(status_code=404, detail="Game not found")

    return game_data

Adding Request Validation

Pydantic provides powerful validation:

from pydantic import BaseModel, Field, validator

class CreateGameRequest(BaseModel):
    league_id: str = Field(..., regex="^(sba|pd)$")
    home_team_id: int = Field(..., gt=0)
    away_team_id: int = Field(..., gt=0)
    game_mode: str = Field(default="friendly", regex="^(ranked|friendly|practice)$")

    @validator('away_team_id')
    def teams_must_differ(cls, v, values):
        if 'home_team_id' in values and v == values['home_team_id']:
            raise ValueError('Home and away teams must be different')
        return v

Adding Query Parameters

For list endpoints with filters:

from typing import Optional

@router.get("/games/")
async def list_games(
    league_id: Optional[str] = None,
    status: Optional[str] = None,
    limit: int = 50,
    offset: int = 0
):
    """
    List games with optional filters

    Query params:
    - league_id: Filter by league ('sba' or 'pd')
    - status: Filter by status ('pending', 'active', 'completed')
    - limit: Number of results (default 50, max 100)
    - offset: Pagination offset (default 0)
    """
    db_ops = DatabaseOperations()
    games = await db_ops.list_games(
        league_id=league_id,
        status=status,
        limit=min(limit, 100),
        offset=offset
    )
    return games

Testing

Manual Testing with Swagger UI

FastAPI automatically generates interactive API documentation:

  1. Start the backend server:

    python -m app.main
    
  2. Open Swagger UI:

    http://localhost:8000/docs
    
  3. Features:

    • View all endpoints grouped by tags
    • See request/response schemas
    • Try out endpoints with test data
    • View response bodies and status codes
    • Copy curl commands

Manual Testing with curl

# Health check
curl http://localhost:8000/api/health

# Database health check
curl http://localhost:8000/api/health/db

# Create token (stub)
curl -X POST http://localhost:8000/api/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "test123",
    "username": "testuser",
    "discord_id": "123456789"
  }'

# List games (stub)
curl http://localhost:8000/api/games/

Unit Testing

Create test files in tests/unit/api/:

import pytest
from httpx import AsyncClient
from app.main import app

@pytest.mark.asyncio
async def test_health_endpoint():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/api/health")
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "healthy"
        assert "timestamp" in data
        assert "version" in data

@pytest.mark.asyncio
async def test_create_token():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.post("/api/auth/token", json={
            "user_id": "test123",
            "username": "testuser",
            "discord_id": "123456789"
        })
        assert response.status_code == 200
        data = response.json()
        assert "access_token" in data
        assert data["token_type"] == "bearer"

Integration Testing

Test with real database:

import pytest
from httpx import AsyncClient
from app.main import app
from app.database.session import init_db

@pytest.mark.integration
@pytest.mark.asyncio
async def test_database_health_endpoint():
    await init_db()

    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/api/health/db")
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "healthy"
        assert data["database"] == "connected"

Troubleshooting

Issue: 422 Unprocessable Entity

Cause: Request body doesn't match Pydantic model schema

Solution:

  1. Check Swagger UI for expected schema
  2. Verify all required fields are present
  3. Check field types match (string vs int, etc.)
  4. Check for custom validators

Example Error:

{
  "detail": [
    {
      "loc": ["body", "user_id"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

Issue: 401 Unauthorized

Cause: Missing or invalid authentication token

Solution:

  1. Verify token is included in Authorization header
  2. Check token format: Bearer <token>
  3. Verify token hasn't expired
  4. Check secret_key matches between token creation and verification

Issue: 500 Internal Server Error

Cause: Unhandled exception in route handler

Solution:

  1. Check backend logs for stack trace
  2. Add try/except blocks with specific error handling
  3. Use HTTPException for expected errors
  4. Log errors with exc_info=True for full stack trace

Example:

try:
    result = await some_operation()
except ValidationError as e:
    # Expected error - return 400
    logger.warning(f"Validation failed: {e}")
    raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
    # Unexpected error - return 500 and log
    logger.error(f"Unexpected error: {e}", exc_info=True)
    raise HTTPException(status_code=500, detail="Internal server error")

Issue: CORS Errors in Frontend

Cause: Frontend origin not in CORS allowed origins

Solution:

  1. Check frontend URL in browser console error
  2. Add origin to .env:
    CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001", "http://your-frontend.com"]
    
  3. Restart backend server

Issue: Route Not Found (404)

Cause: Route not registered or incorrect path

Solution:

  1. Verify router is imported and registered in app/main.py
  2. Check prefix matches: /api/games/ vs /api/game/
  3. Verify route path in decorator matches request
  4. Check method matches (GET vs POST)

Example Registration:

# In app/main.py
from app.api.routes import games

app.include_router(
    games.router,
    prefix="/api/games",  # All routes will be /api/games/*
    tags=["games"]
)

Best Practices

1. Keep Route Handlers Thin

Route handlers should orchestrate, not implement logic:

# ❌ Bad - business logic in route handler
@router.post("/games/")
async def create_game(request: CreateGameRequest):
    # Validate teams exist
    # Check user permissions
    # Create game record
    # Initialize state
    # Send notifications
    # ... 100 lines of logic

# ✅ Good - orchestrate using service layer
@router.post("/games/")
async def create_game(request: CreateGameRequest, user = Depends(get_current_user)):
    game_service = GameService()
    game = await game_service.create_game(request, user['user_id'])
    return game

2. Use Explicit Response Models

Always specify response_model for type safety and documentation:

# ❌ Bad - no response model
@router.get("/games/{game_id}")
async def get_game(game_id: str):
    return {"game_id": game_id, "status": "active"}

# ✅ Good - explicit response model
@router.get("/games/{game_id}", response_model=GameResponse)
async def get_game(game_id: str):
    return GameResponse(game_id=game_id, status="active")

3. Document Endpoints

Use docstrings for endpoint documentation:

@router.get("/games/", response_model=List[GameListItem])
async def list_games(
    league_id: Optional[str] = None,
    status: Optional[str] = None
):
    """
    List games with optional filters.

    Query Parameters:
    - league_id: Filter by league ('sba' or 'pd')
    - status: Filter by status ('pending', 'active', 'completed')

    Returns:
    - List of GameListItem objects

    Raises:
    - 400: Invalid filter parameters
    - 500: Database error
    """
    ...

4. Handle Errors Gracefully

Return meaningful error messages:

@router.get("/games/{game_id}")
async def get_game(game_id: str):
    game = await db_ops.get_game(game_id)

    if not game:
        raise HTTPException(
            status_code=404,
            detail=f"Game {game_id} not found"
        )

    return game

5. Use Dependency Injection

Share common logic via dependencies:

from fastapi import Depends

async def get_db_ops():
    """Dependency for database operations"""
    return DatabaseOperations()

@router.get("/games/{game_id}")
async def get_game(
    game_id: str,
    db_ops: DatabaseOperations = Depends(get_db_ops)
):
    # db_ops injected automatically
    game = await db_ops.get_game(game_id)
    return game

6. Version Your API

When making breaking changes, use API versioning:

# Option 1: URL path versioning
app.include_router(games_v1.router, prefix="/api/v1/games")
app.include_router(games_v2.router, prefix="/api/v2/games")

# Option 2: Header versioning (more complex)
@router.get("/games/")
async def list_games(api_version: str = Header(default="v1")):
    if api_version == "v2":
        return new_format_games()
    else:
        return legacy_format_games()

References

  • FastAPI Documentation: https://fastapi.tiangolo.com/
  • Pydantic Documentation: https://docs.pydantic.dev/
  • Main Application: app/main.py (router registration)
  • Settings: app/config.py (application configuration)
  • Database Layer: app/database/operations.py (DB operations)
  • WebSocket Layer: app/websocket/handlers.py (real-time events)
  • Authentication: app/utils/auth.py (JWT utilities)

Future Enhancements

Phase 1 (Weeks 1-4)

  • Complete Discord OAuth flow in auth.py
  • Add authentication middleware
  • Create user session endpoints
  • Add token refresh mechanism

Phase 2+ (Weeks 5-13)

  • Implement game CRUD in games.py
  • Add game listing with filters and pagination
  • Create team management endpoints
  • Add roster/lineup endpoints
  • Create statistics/analytics endpoints
  • Add game history endpoints
  • Implement WebSocket-REST synchronization

Beyond MVP

  • Add comprehensive API rate limiting
  • Implement API key authentication for external integrations
  • Create admin endpoints for moderation
  • Add bulk operations endpoints
  • Create export endpoints (CSV, JSON)
  • Add advanced search and filtering

Note: This directory is currently 33% complete (health endpoints only). Most endpoints are stubs awaiting Phase 2+ implementation. Focus on health endpoints for current operational needs.

Current Phase: Phase 2 - Week 8 Last Updated: 2025-10-31