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

39 KiB

Architecture Guide

This document provides a comprehensive overview of the backend architecture, design patterns, and structural organization.

Table of Contents

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:

@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:

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:

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:

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:

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:

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

# 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

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

@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:

# 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:

# 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

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:

# 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

{
    "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)

# 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

@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

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:

# 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

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:

@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:

{
  "success": false,
  "errors": [
    {
      "code": "AUTH_001",
      "message": "Invalid credentials",
      "field": "email"
    }
  ]
}

API Design

Versioning

API versioned via URL path:

# 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:

# 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:

# 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

# 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

# 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:

# 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:

# Run tests with coverage
pytest --cov=app --cov-report=html --cov-report=term

# View coverage report
open htmlcov/index.html

Security Architecture

Security Headers

# 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

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