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

907 lines
23 KiB
Markdown

# 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**:
```json
// 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:
```python
from fastapi import APIRouter
router = APIRouter()
@router.get("/endpoint")
async def handler():
...
```
Routers are registered in `app/main.py`:
```python
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:
```python
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`:
```python
@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:
```python
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:
```python
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:
```python
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/`)
```python
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/`)
```python
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`)
```python
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`)
```python
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:
```typescript
// 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)
```bash
# If creating new module
touch app/api/routes/teams.py
```
2. **Define Pydantic models** for request/response
```python
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**
```python
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**
```python
from app.api.routes import teams
app.include_router(teams.router, prefix="/api/teams", tags=["teams"])
```
5. **Test the endpoint**
```bash
# 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:
```python
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:
```python
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:
```python
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:
```python
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:
```bash
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
```bash
# 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/`:
```python
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:
```python
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**:
```json
{
"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**:
```python
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`:
```bash
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**:
```python
# 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:
```python
# ❌ 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:
```python
# ❌ 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:
```python
@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:
```python
@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:
```python
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:
```python
# 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