## 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)
907 lines
23 KiB
Markdown
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
|