Files
fast-next-template/backend/docs/ARCHITECTURE.md
Felipe Cardoso 7ff00426f2 Add detailed OAuth documentation and configuration examples
- Updated `ARCHITECTURE.md` with thorough explanations of OAuth Consumer and Provider modes, supported flows, security features, and endpoints.
- Enhanced `.env.template` with environment variables for OAuth Provider mode setup.
- Expanded `README.md` to highlight OAuth Provider mode capabilities and MCP integration features.
- Added OAuth configuration section to `AGENTS.md`, including key settings for both social login and provider mode.
2025-11-26 13:38:55 +01:00

1250 lines
39 KiB
Markdown

# Architecture Guide
This document provides a comprehensive overview of the backend architecture, design patterns, and structural organization.
## Table of Contents
- [Overview](#overview)
- [Technology Stack](#technology-stack)
- [Project Structure](#project-structure)
- [Layered Architecture](#layered-architecture)
- [Database Architecture](#database-architecture)
- [Authentication & Authorization](#authentication--authorization)
- [Error Handling](#error-handling)
- [API Design](#api-design)
- [Background Jobs](#background-jobs)
- [Testing Strategy](#testing-strategy)
- [Security Architecture](#security-architecture)
- [Performance Considerations](#performance-considerations)
## Overview
This FastAPI backend application follows a **clean layered architecture** pattern with clear separation of concerns. The architecture is designed to be:
- **Scalable**: Can handle growing data and user load
- **Maintainable**: Easy to understand, modify, and extend
- **Testable**: Each layer can be tested independently
- **Secure**: Security built into every layer
- **Type-safe**: Comprehensive type hints throughout
### Key Architectural Principles
1. **Separation of Concerns**: Each layer has a single, well-defined responsibility
2. **Dependency Injection**: Dependencies are injected rather than hard-coded
3. **Single Responsibility**: Each module, class, and function does one thing well
4. **Open/Closed Principle**: Open for extension, closed for modification
5. **Interface Segregation**: Clients depend only on interfaces they use
6. **Dependency Inversion**: Depend on abstractions, not concretions
## Technology Stack
### Core Framework
- **FastAPI 0.115.8+**: Modern async web framework
- Automatic OpenAPI documentation
- Built-in data validation
- High performance (based on Starlette and Pydantic)
- Type safety with Python 3.10+
- **Uvicorn**: ASGI server for production deployment
- Async request handling
- WebSocket support
- HTTP/2 support
### Database Layer
- **SQLAlchemy 2.0+**: ORM and database toolkit
- Supports async operations
- Type-safe query building
- Migration support via Alembic
- **PostgreSQL**: Primary production database
- ACID compliance
- Advanced indexing
- JSON support
- Full-text search capabilities
- **Alembic**: Database migration tool
- Version-controlled schema changes
- Automatic migration generation
- Rollback support
### Data Validation
- **Pydantic 2.10+**: Data validation using Python type hints
- Fast validation (Rust core)
- Automatic JSON schema generation
- Custom validators
- Type coercion
### Authentication & Security
- **python-jose**: JWT token generation and validation
- Cryptographic signing
- Token expiration handling
- Claims validation
- **passlib + bcrypt**: Password hashing
- Industry-standard bcrypt algorithm
- Configurable cost factor
- Salt generation
### Additional Features
- **SlowAPI**: Rate limiting
- Per-IP rate limiting
- Per-route configuration
- Redis backend support (optional)
- **APScheduler**: Background job scheduling
- Cron-style scheduling
- Interval-based jobs
- Async job support
- **starlette-csrf**: CSRF protection
- Token-based CSRF prevention
- Cookie-based tokens
## Project Structure
```
backend/
├── app/
│ ├── alembic/ # Database migrations
│ │ ├── versions/ # Migration files
│ │ └── env.py # Migration environment
│ │
│ ├── api/ # API layer
│ │ ├── dependencies/ # Dependency injection
│ │ │ ├── auth.py # Authentication dependencies
│ │ │ └── permissions.py # Authorization dependencies
│ │ ├── routes/ # API endpoints
│ │ │ ├── auth.py # Authentication routes
│ │ │ ├── users.py # User management routes
│ │ │ ├── sessions.py # Session management routes
│ │ │ ├── organizations.py # Organization routes
│ │ │ └── admin.py # Admin routes
│ │ └── main.py # API router aggregation
│ │
│ ├── core/ # Core functionality
│ │ ├── auth.py # JWT and password utilities
│ │ ├── config.py # Application configuration
│ │ ├── database.py # Database connection
│ │ ├── exceptions.py # Custom exception classes
│ │ └── middleware.py # Custom middleware
│ │
│ ├── crud/ # Database operations
│ │ ├── base.py # Generic CRUD base class
│ │ ├── user.py # User CRUD operations
│ │ ├── session.py # Session CRUD operations
│ │ └── organization.py # Organization CRUD
│ │
│ ├── models/ # SQLAlchemy models
│ │ ├── base.py # Base model with mixins
│ │ ├── user.py # User model
│ │ ├── user_session.py # Session tracking model
│ │ ├── organization.py # Organization model
│ │ └── user_organization.py # Many-to-many relationship
│ │
│ ├── schemas/ # Pydantic schemas
│ │ ├── common.py # Common schemas (pagination, etc.)
│ │ ├── errors.py # Error response schemas
│ │ ├── users.py # User schemas
│ │ ├── sessions.py # Session schemas
│ │ └── organizations.py # Organization schemas
│ │
│ ├── services/ # Business logic
│ │ ├── auth_service.py # Authentication service
│ │ ├── email_service.py # Email service
│ │ └── session_cleanup.py # Background cleanup
│ │
│ ├── utils/ # Utility functions
│ │ ├── security.py # Security utilities
│ │ ├── device.py # Device detection
│ │ └── test_utils.py # Testing utilities
│ │
│ ├── init_db.py # Database initialization
│ └── main.py # Application entry point
├── tests/ # Test suite
│ ├── api/ # Integration tests
│ ├── crud/ # CRUD tests
│ ├── models/ # Model tests
│ ├── services/ # Service tests
│ └── conftest.py # Test configuration
├── docs/ # Documentation
│ ├── ARCHITECTURE.md # This file
│ ├── CODING_STANDARDS.md # Coding standards
│ └── FEATURE_EXAMPLE.md # Feature implementation guide
├── requirements.txt # Python dependencies
├── pytest.ini # Pytest configuration
├── .coveragerc # Coverage configuration
└── alembic.ini # Alembic configuration
```
## Layered Architecture
The application follows a strict 5-layer architecture:
```
┌─────────────────────────────────────────────────────────────┐
│ API Layer (routes/) │
│ - HTTP endpoints │
│ - Request/response handling │
│ - OpenAPI documentation │
│ - Rate limiting │
└──────────────────────────┬──────────────────────────────────┘
│ calls
┌──────────────────────────▼──────────────────────────────────┐
│ Dependencies (dependencies/) │
│ - Authentication (get_current_user) │
│ - Authorization (permission checks) │
│ - Database session injection │
│ - Request context │
└──────────────────────────┬──────────────────────────────────┘
│ injects
┌──────────────────────────▼──────────────────────────────────┐
│ Service Layer (services/) │
│ - Business logic │
│ - Multi-step operations │
│ - Cross-cutting concerns │
│ - External service integration │
└──────────────────────────┬──────────────────────────────────┘
│ calls
┌──────────────────────────▼──────────────────────────────────┐
│ CRUD Layer (crud/) │
│ - Database operations │
│ - Query building │
│ - Transaction management │
│ - Error handling │
└──────────────────────────┬──────────────────────────────────┘
│ uses
┌──────────────────────────▼──────────────────────────────────┐
│ Data Layer (models/ + schemas/) │
│ - SQLAlchemy models (database structure) │
│ - Pydantic schemas (validation) │
│ - Type definitions │
└─────────────────────────────────────────────────────────────┘
```
### Layer Details
#### 1. API Layer (`app/api/routes/`)
**Responsibility**: Handle HTTP requests and responses
**Key Functions**:
- Define API endpoints and routes
- Handle request validation via Pydantic schemas
- Return structured responses
- Apply rate limiting
- Generate OpenAPI documentation
- Handle file uploads/downloads
**Example**:
```python
@router.get(
"/me",
response_model=UserResponse,
summary="Get current user"
)
@limiter.limit("30/minute")
async def get_current_user_info(
request: Request,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
) -> UserResponse:
"""Get the currently authenticated user's information."""
return current_user
```
**Rules**:
- Should NOT contain business logic
- Should NOT directly perform database operations (use CRUD or services)
- Must validate all input via Pydantic schemas
- Must specify response models
- Should apply appropriate rate limits
#### 2. Dependencies Layer (`app/api/dependencies/`)
**Responsibility**: Provide reusable dependency injection functions
**Key Functions**:
- Authenticate users from JWT tokens
- Check user permissions and roles
- Inject database sessions
- Provide request context
**Example**:
```python
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> User:
"""
Extract and validate user from JWT token.
Raises:
AuthenticationError: If token is invalid or user not found
"""
try:
payload = decode_access_token(token)
user_id = UUID(payload.get("sub"))
except Exception:
raise AuthenticationError("Invalid authentication credentials")
user = user_crud.get(db, id=user_id)
if not user:
raise AuthenticationError("User not found")
return user
```
**Rules**:
- Should be pure functions
- Should raise appropriate exceptions
- Should be reusable across multiple routes
- Must handle errors gracefully
#### 3. Service Layer (`app/services/`)
**Responsibility**: Implement complex business logic
**Key Functions**:
- Orchestrate multiple CRUD operations
- Implement business rules
- Handle external service integration
- Coordinate transactions
**Example**:
```python
class AuthService:
"""Authentication service with business logic."""
def login(
self,
db: Session,
email: str,
password: str,
request: Request
) -> dict:
"""
Authenticate user and create session.
Business logic:
1. Validate credentials
2. Create session with device info
3. Generate tokens
4. Return tokens and user info
"""
# Validate credentials
user = user_crud.get_by_email(db, email=email)
if not user or not verify_password(password, user.hashed_password):
raise AuthenticationError("Invalid credentials")
if not user.is_active:
raise AuthenticationError("Account is inactive")
# Extract device info
device_info = extract_device_info(request)
# Create session
session = session_crud.create_session(
db,
user_id=user.id,
device_info=device_info
)
# Generate tokens
access_token = create_access_token(subject=str(user.id))
refresh_token = create_refresh_token(
subject=str(user.id),
jti=str(session.refresh_token_jti)
)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"user": user
}
```
**Rules**:
- Contains business logic, not just data operations
- Can call multiple CRUD operations
- Should handle complex workflows
- Must maintain data consistency
- Should use transactions when needed
#### 4. CRUD Layer (`app/crud/`)
**Responsibility**: Database operations and queries
**Key Functions**:
- Create, read, update, delete operations
- Build database queries
- Handle database errors
- Manage soft deletes
- Implement pagination and filtering
**Example**:
```python
class CRUDSession(CRUDBase[UserSession, SessionCreate, SessionUpdate]):
"""CRUD operations for user sessions."""
def get_by_jti(self, db: Session, jti: UUID) -> Optional[UserSession]:
"""Get session by refresh token JTI."""
try:
return (
db.query(UserSession)
.filter(UserSession.refresh_token_jti == jti)
.first()
)
except Exception as e:
logger.error(f"Error getting session by JTI: {str(e)}")
return None
def get_active_by_jti(
self,
db: Session,
jti: UUID
) -> Optional[UserSession]:
"""Get active session by refresh token JTI."""
session = self.get_by_jti(db, jti=jti)
if session and session.is_active and not session.is_expired:
return session
return None
def deactivate(self, db: Session, session_id: UUID) -> bool:
"""Deactivate a session (logout)."""
try:
session = self.get(db, id=session_id)
if not session:
return False
session.is_active = False
db.commit()
logger.info(f"Session {session_id} deactivated")
return True
except Exception as e:
db.rollback()
logger.error(f"Error deactivating session: {str(e)}")
return False
```
**Rules**:
- Should NOT contain business logic
- Must handle database exceptions
- Must use parameterized queries (SQLAlchemy does this)
- Should log all database errors
- Must rollback on errors
- Should use soft deletes when possible
#### 5. Data Layer (`app/models/` + `app/schemas/`)
**Responsibility**: Define data structures
##### Models (`app/models/`)
Database schema definition using SQLAlchemy:
```python
from app.models.base import Base, UUIDMixin, TimestampMixin
class User(Base, UUIDMixin, TimestampMixin):
"""User model."""
__tablename__ = "users"
email = Column(String(255), unique=True, nullable=False, index=True)
hashed_password = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
is_superuser = Column(Boolean, default=False, nullable=False)
deleted_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
sessions = relationship("UserSession", back_populates="user", cascade="all, delete-orphan")
# Indexes
__table_args__ = (
Index("idx_user_email_active", "email", "is_active"),
)
```
##### Schemas (`app/schemas/`)
Data validation and serialization using Pydantic:
```python
from pydantic import BaseModel, Field, ConfigDict
class UserBase(BaseModel):
"""Base user schema with common fields."""
email: str = Field(..., description="User's email address")
class UserCreate(UserBase):
"""Schema for creating a user."""
password: str = Field(..., min_length=8)
class UserUpdate(UserBase):
"""Schema for updating a user."""
email: Optional[str] = None
password: Optional[str] = None
class UserResponse(UserBase):
"""Schema for user API responses."""
model_config = ConfigDict(from_attributes=True)
id: UUID
is_active: bool
created_at: datetime
```
**Rules**:
- Models define database structure
- Schemas define API contracts
- Never expose sensitive fields (passwords, tokens)
- Use mixins for common fields
- Define appropriate indexes
## Database Architecture
### Connection Management
```python
# app/core/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Connection pooling configuration
engine = create_engine(
DATABASE_URL,
pool_size=20, # Number of persistent connections
max_overflow=50, # Additional connections when pool exhausted
pool_timeout=30, # Seconds to wait for connection
pool_recycle=3600, # Recycle connections after 1 hour
pool_pre_ping=True, # Verify connections before use
)
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
```
### Session Management
#### Dependency Injection Pattern
```python
def get_db() -> Generator[Session, None, None]:
"""
Database session dependency for FastAPI routes.
Automatically commits on success, rolls back on error.
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
# Usage in routes
@router.get("/users")
def list_users(db: Session = Depends(get_db)):
return user_crud.get_multi(db)
```
#### Context Manager Pattern
```python
@contextmanager
def transaction_scope() -> Generator[Session, None, None]:
"""
Context manager for database transactions.
Use for complex operations requiring multiple steps.
Automatically commits on success, rolls back on error.
"""
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
# Usage in services
def complex_operation():
with transaction_scope() as db:
user = user_crud.create(db, obj_in=user_data)
session = session_crud.create(db, session_data)
return user, session
```
### Model Mixins
Common functionality shared across models:
```python
# app/models/base.py
class UUIDMixin:
"""Add UUID primary key to model."""
id = Column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
unique=True,
nullable=False
)
class TimestampMixin:
"""Add created_at and updated_at timestamps."""
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False
)
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
nullable=True
)
# All models inherit both mixins
class User(Base, UUIDMixin, TimestampMixin):
__tablename__ = "users"
# ...
```
### Migration System
Database migrations managed by Alembic:
```bash
# Create a new migration
alembic revision --autogenerate -m "Add user_sessions table"
# Apply migrations
alembic upgrade head
# Rollback one migration
alembic downgrade -1
# Show migration history
alembic history
```
### Indexing Strategy
```python
class UserSession(Base):
__tablename__ = "user_sessions"
# Single-column indexes
user_id = Column(UUID, ForeignKey("users.id"), index=True)
refresh_token_jti = Column(UUID, unique=True, index=True)
# Composite indexes for common queries
__table_args__ = (
Index("idx_user_session_active", "user_id", "is_active"),
Index("idx_session_expiry", "expires_at", "is_active"),
)
```
**Indexing Guidelines**:
- Index foreign keys
- Index columns used in WHERE clauses
- Index columns used in JOIN conditions
- Use composite indexes for multi-column queries
- Monitor query performance with EXPLAIN
## Authentication & Authorization
### JWT Token System
Two-token strategy for security:
```python
# Access Token (short-lived)
access_token = create_access_token(
subject=str(user.id),
additional_claims={"is_superuser": user.is_superuser}
)
# Expiry: 15 minutes
# Used for: API authentication
# Stored: Client-side (memory or secure storage)
# Refresh Token (long-lived)
refresh_token = create_refresh_token(
subject=str(user.id),
jti=str(session.refresh_token_jti) # Session tracking
)
# Expiry: 7 days
# Used for: Getting new access tokens
# Stored: HttpOnly cookie or secure storage
```
### Token Claims
```python
{
"sub": "user-uuid-here", # Subject (user ID)
"type": "access", # Token type
"exp": 1234567890, # Expiration timestamp
"iat": 1234567800, # Issued at timestamp
"is_superuser": false, # User role
"jti": "session-uuid-here" # JWT ID (for refresh tokens)
}
```
### Authentication Flow
```
┌─────────┐ ┌─────────┐
│ Client │ │ Backend │
└────┬────┘ └────┬────┘
│ │
│ POST /auth/login │
│ {email, password} │
│───────────────────────────────────────────────>│
│ │
│ Verify credentials
│ Create session
│ Generate tokens
│ │
│ {access_token, refresh_token, user} │
│<───────────────────────────────────────────────│
│ │
│ GET /api/v1/users/me │
│ Authorization: Bearer {access_token} │
│───────────────────────────────────────────────>│
│ │
│ Validate token
│ Get user
│ │
│ {user data} │
│<───────────────────────────────────────────────│
│ │
│ (after 15 minutes) │
│ POST /auth/refresh │
│ {refresh_token} │
│───────────────────────────────────────────────>│
│ │
│ Validate refresh token
│ Check session active
│ Generate new tokens
│ │
│ {access_token, refresh_token} │
│<───────────────────────────────────────────────│
│ │
```
### Authorization Patterns
#### Role-Based Access Control (RBAC)
```python
# Superuser check
@router.post("/admin/users")
def admin_endpoint(
current_user: User = Depends(get_current_superuser)
):
"""Only superusers can access this endpoint."""
pass
# Active user check
@router.get("/users/me")
def get_profile(
current_user: User = Depends(get_current_active_user)
):
"""Only active users can access this endpoint."""
pass
```
#### Resource Ownership
```python
@router.delete("/sessions/{session_id}")
def revoke_session(
session_id: UUID,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Users can only revoke their own sessions."""
session = session_crud.get(db, id=session_id)
if not session:
raise NotFoundError("Session not found")
# Check ownership
if session.user_id != current_user.id:
raise AuthorizationError("You can only revoke your own sessions")
session_crud.deactivate(db, session_id=session_id)
return MessageResponse(success=True, message="Session revoked")
```
#### Organization-Based Permissions
```python
from app.api.dependencies.permissions import require_org_admin
@router.post("/organizations/{org_id}/members")
def add_member(
org_id: UUID,
member_data: MemberCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
_: None = Depends(require_org_admin(org_id)) # Permission check
):
"""Only organization admins can add members."""
pass
```
### OAuth Integration
The system supports two OAuth modes:
#### OAuth Consumer Mode (Social Login)
Users can authenticate via Google or GitHub OAuth providers:
```python
# Get authorization URL with PKCE support
GET /oauth/authorize/{provider}?redirect_uri=https://yourapp.com/callback
# Handle callback and exchange code for tokens
POST /oauth/callback/{provider}
{
"code": "authorization_code_from_provider",
"state": "csrf_state_token"
}
```
**Security Features:**
- PKCE (S256) for Google
- State parameter for CSRF protection
- Nonce for Google OIDC replay attack prevention
- Google ID token signature verification via JWKS
- Email normalization to prevent account duplication
- Auto-linking by email (configurable)
#### OAuth Provider Mode (MCP Integration)
Full OAuth 2.0 Authorization Server for third-party clients (RFC compliant):
```
┌─────────────┐ ┌─────────────┐
│ MCP Client │ │ Backend │
└──────┬──────┘ └──────┬──────┘
│ │
│ GET /.well-known/oauth-authorization-server│
│─────────────────────────────────────────────>│
│ {metadata} │
│<─────────────────────────────────────────────│
│ │
│ GET /oauth/provider/authorize │
│ ?response_type=code&client_id=... │
│ &redirect_uri=...&code_challenge=... │
│─────────────────────────────────────────────>│
│ │
│ (User consents) │
│ │
│ 302 redirect_uri?code=AUTH_CODE&state=... │
│<─────────────────────────────────────────────│
│ │
│ POST /oauth/provider/token │
│ {grant_type=authorization_code, │
│ code=AUTH_CODE, code_verifier=...} │
│─────────────────────────────────────────────>│
│ │
│ {access_token, refresh_token, expires_in} │
│<─────────────────────────────────────────────│
│ │
```
**Endpoints:**
- `GET /.well-known/oauth-authorization-server` - RFC 8414 metadata
- `GET /oauth/provider/authorize` - Authorization endpoint
- `POST /oauth/provider/token` - Token endpoint (authorization_code, refresh_token)
- `POST /oauth/provider/revoke` - RFC 7009 token revocation
- `POST /oauth/provider/introspect` - RFC 7662 token introspection
**Security Features:**
- PKCE S256 required for public clients (plain method rejected)
- Authorization codes are single-use with 10-minute expiry
- Code reuse detection triggers security incident (all tokens revoked)
- Refresh token rotation on use
- Opaque refresh tokens (hashed in database)
- JWT access tokens with standard claims
- Consent management per client
## Error Handling
### Exception Hierarchy
```python
class APIException(Exception):
"""Base exception for all API errors."""
def __init__(
self,
message: str,
status_code: int,
error_code: str,
field: Optional[str] = None
):
self.message = message
self.status_code = status_code
self.error_code = error_code
self.field = field
# Specific exceptions
class AuthenticationError(APIException):
"""401 Unauthorized"""
def __init__(self, message: str, error_code: str = "AUTH_001", field: Optional[str] = None):
super().__init__(message, 401, error_code, field)
class AuthorizationError(APIException):
"""403 Forbidden"""
def __init__(self, message: str, error_code: str = "AUTH_002", field: Optional[str] = None):
super().__init__(message, 403, error_code, field)
class NotFoundError(APIException):
"""404 Not Found"""
def __init__(self, message: str, error_code: str = "NOT_001", field: Optional[str] = None):
super().__init__(message, 404, error_code, field)
class DuplicateError(APIException):
"""409 Conflict"""
def __init__(self, message: str, error_code: str = "DUP_001", field: Optional[str] = None):
super().__init__(message, 409, error_code, field)
```
### Global Exception Handlers
Registered in `app/main.py`:
```python
@app.exception_handler(APIException)
async def api_exception_handler(request: Request, exc: APIException):
"""Handle custom API exceptions."""
return JSONResponse(
status_code=exc.status_code,
content={
"success": False,
"errors": [
{
"code": exc.error_code,
"message": exc.message,
"field": exc.field
}
]
}
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""Handle Pydantic validation errors."""
errors = []
for error in exc.errors():
errors.append({
"code": "VAL_001",
"message": error["msg"],
"field": ".".join(str(x) for x in error["loc"])
})
return JSONResponse(
status_code=422,
content={"success": False, "errors": errors}
)
```
### Error Response Format
All errors follow this structure:
```json
{
"success": false,
"errors": [
{
"code": "AUTH_001",
"message": "Invalid credentials",
"field": "email"
}
]
}
```
## API Design
### Versioning
API versioned via URL path:
```python
# app/api/main.py
api_router = APIRouter(prefix="/api/v1")
api_router.include_router(auth_router, tags=["auth"])
api_router.include_router(users_router, tags=["users"])
api_router.include_router(sessions_router, tags=["sessions"])
```
### Pagination
Consistent pagination across all list endpoints:
```python
# Request
GET /api/v1/users?page=1&limit=20
# Response
{
"data": [...],
"pagination": {
"total": 100,
"page": 1,
"page_size": 20,
"total_pages": 5,
"has_next": true,
"has_prev": false
}
}
```
### Rate Limiting
Applied per-endpoint based on sensitivity:
```python
# Read operations - 60/minute
@limiter.limit("60/minute")
@router.get("/users")
# Write operations - 10/minute
@limiter.limit("10/minute")
@router.post("/users")
# Authentication - 5/minute
@limiter.limit("5/minute")
@router.post("/auth/login")
```
## Background Jobs
### APScheduler Integration
```python
# app/main.py
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app.services.session_cleanup import cleanup_expired_sessions
scheduler = AsyncIOScheduler()
@app.on_event("startup")
async def startup_event():
"""Start background jobs on application startup."""
if not settings.IS_TEST: # Don't run in tests
scheduler.add_job(
cleanup_expired_sessions,
"cron",
hour=2, # Run at 2 AM daily
id="cleanup_expired_sessions"
)
scheduler.start()
logger.info("Background jobs started")
@app.on_event("shutdown")
async def shutdown_event():
"""Stop background jobs on application shutdown."""
scheduler.shutdown()
```
### Job Implementation
```python
# app/services/session_cleanup.py
async def cleanup_expired_sessions():
"""
Clean up expired sessions.
Runs daily at 2 AM. Removes sessions expired for more than 30 days.
"""
try:
with transaction_scope() as db:
count = session_crud.cleanup_expired(db, keep_days=30)
logger.info(f"Cleaned up {count} expired sessions")
except Exception as e:
logger.error(f"Error cleaning up sessions: {str(e)}", exc_info=True)
```
## Testing Strategy
### Test Pyramid
```
┌─────────────┐
│ E2E Tests │ ← Few, high-level
├─────────────┤
│Integration │ ← API endpoint tests
│ Tests │
├─────────────┤
│ Unit │ ← CRUD, services, utilities
│ Tests │
└─────────────┘
```
### Test Database
Use SQLite in-memory for fast tests:
```python
# tests/conftest.py
@pytest.fixture(scope="session")
def test_engine():
"""Create test database engine."""
return create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False}
)
@pytest.fixture
def db_session(test_engine):
"""Create a fresh database session for each test."""
Base.metadata.create_all(bind=test_engine)
Session = sessionmaker(bind=test_engine)
session = Session()
yield session
session.close()
Base.metadata.drop_all(bind=test_engine)
```
### Test Coverage
Aim for 80%+ coverage:
```bash
# Run tests with coverage
pytest --cov=app --cov-report=html --cov-report=term
# View coverage report
open htmlcov/index.html
```
## Security Architecture
### Security Headers
```python
# Content Security Policy
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
# CSP
response.headers["Content-Security-Policy"] = "default-src 'self'"
# Other security headers
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
return response
```
### CORS Configuration
```python
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"https://yourdomain.com"
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["*"],
expose_headers=["X-Total-Count"]
)
```
### Password Requirements
- Minimum 8 characters
- Bcrypt hashing with cost factor 12
- No password history (can be added if needed)
### Session Security
- Per-device session tracking
- Automatic session expiration
- Manual session revocation
- Token rotation on refresh
## Performance Considerations
### Database Connection Pooling
- Pool size: 20 connections
- Max overflow: 50 connections
- Connection recycling every hour
- Pre-ping for connection health
### Query Optimization
- Eager loading for relationships
- Appropriate indexes on frequently queried columns
- Query result pagination
- Avoid N+1 queries
### Caching Strategy
Currently no caching implemented. Consider adding:
- Redis for session storage
- Response caching for read-heavy endpoints
- Query result caching
### Rate Limiting
Protects against abuse and DoS attacks:
- IP-based rate limiting
- Per-endpoint limits
- Can be extended with Redis for distributed systems
## Conclusion
This architecture provides a solid foundation for a scalable, maintainable, and secure FastAPI application. Key benefits:
- **Clear separation of concerns**: Each layer has a specific responsibility
- **Type safety**: Comprehensive type hints throughout
- **Security**: Built-in authentication, authorization, and security best practices
- **Testability**: Each layer can be tested independently
- **Maintainability**: Clean code structure and comprehensive documentation
- **Scalability**: Connection pooling, rate limiting, and efficient queries
For implementation examples, see:
- **Coding Standards**: `backend/docs/CODING_STANDARDS.md`
- **Feature Example**: `backend/docs/FEATURE_EXAMPLE.md`