Files
fast-next-template/backend/docs/CODING_STANDARDS.md
Felipe Cardoso 2bbe925cef Clean up Alembic migrations
- Removed outdated and redundant Alembic migration files to streamline the migration directory. This improves maintainability and eliminates duplicate or unused scripts.
2025-11-27 09:12:30 +01:00

30 KiB

Coding Standards

This document outlines the coding standards and best practices for the FastAPI backend application.

Table of Contents

General Principles

1. Follow PEP 8

All Python code should follow PEP 8 style guidelines:

  • Use 4 spaces for indentation (never tabs)
  • Maximum line length: 88 characters (Black formatter default)
  • Two blank lines between top-level functions and classes
  • One blank line between methods in a class

2. Type Hints

Always use type hints for function parameters and return values:

from typing import Optional
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select

async def get_user(db: AsyncSession, user_id: UUID) -> Optional[User]:
    """Retrieve a user by ID."""
    result = await db.execute(select(User).where(User.id == user_id))
    return result.scalar_one_or_none()

Modern Python Type Hints:

  • Use list[T] instead of List[T] (Python 3.10+)
  • Use dict[K, V] instead of Dict[K, V]
  • Use T | None instead of Optional[T]
  • Use str | int instead of Union[str, int]

3. Docstrings

Use Google-style docstrings for all public functions, classes, and methods:

def create_user(db: Session, user_in: UserCreate) -> User:
    """
    Create a new user in the database.

    Args:
        db: Database session
        user_in: User creation schema with validated data

    Returns:
        The newly created user object

    Raises:
        DuplicateError: If user with email already exists
        ValidationException: If validation fails
    """
    # Implementation

4. Code Formatting

Use automated formatters:

  • Black: Code formatting
  • isort: Import sorting
  • flake8: Linting

Run before committing:

black app tests
isort app tests
flake8 app tests

Code Organization

Layer Separation

Follow the 5-layer architecture strictly:

API Layer (routes/)
    ↓ calls
Dependencies (dependencies/)
    ↓ injects
Service Layer (services/)
    ↓ calls
CRUD Layer (crud/)
    ↓ uses
Models & Schemas (models/, schemas/)

Rules:

  • Routes should NOT directly call CRUD operations (use services when business logic is needed)
  • CRUD operations should NOT contain business logic
  • Models should NOT import from higher layers
  • Each layer should only depend on the layer directly below it

File Organization

# Standard import order:
# 1. Standard library
import logging
from datetime import datetime
from typing import Optional, List

# 2. Third-party packages
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

# 3. Local application imports
from app.api.dependencies.auth import get_current_user
from app.crud import user_crud
from app.models.user import User
from app.schemas.users import UserResponse, UserCreate

Naming Conventions

Variables and Functions

# Use snake_case for variables and functions
user_id = "123"
def get_user_by_email(email: str) -> Optional[User]:
    pass

# Use UPPER_CASE for constants
MAX_LOGIN_ATTEMPTS = 5
DEFAULT_PAGE_SIZE = 20

Classes

# Use PascalCase for class names
class UserSession:
    pass

class AuthenticationError(APIException):
    pass

Database Tables and Columns

# Use snake_case for table and column names
class User(Base):
    __tablename__ = "users"

    first_name = Column(String(100))
    last_name = Column(String(100))
    created_at = Column(DateTime)

API Endpoints

# Use kebab-case for URL paths
@router.get("/user-sessions")
@router.post("/password-reset")
@router.delete("/user-sessions/expired")

Files and Directories

# Use snake_case for file and directory names
user_session.py
auth_service.py
email_service.py

Error Handling

Use Custom Exceptions

Always use custom exceptions from app.core.exceptions:

from app.core.exceptions import (
    NotFoundError,
    DuplicateError,
    AuthenticationError,
    AuthorizationError,
    ValidationException,
    DatabaseError
)

# Good
if not user:
    raise NotFoundError(
        message="User not found",
        error_code="USER_001",
        field="user_id"
    )

# Bad - Don't use generic exceptions
if not user:
    raise ValueError("User not found")

Error Handling Pattern

Always follow this pattern in CRUD operations (Async version):

from sqlalchemy.exc import IntegrityError, OperationalError, DataError
from sqlalchemy.ext.asyncio import AsyncSession

async def create_user(db: AsyncSession, user_in: UserCreate) -> User:
    """Create a new user."""
    try:
        db_user = User(**user_in.model_dump())
        db.add(db_user)
        await db.commit()
        await db.refresh(db_user)
        logger.info(f"User created: {db_user.id}")
        return db_user

    except IntegrityError as e:
        await db.rollback()
        logger.error(f"Integrity error creating user: {str(e)}")

        # Check for specific constraint violations
        if "unique constraint" in str(e).lower():
            if "email" in str(e).lower():
                raise DuplicateError(
                    message="User with this email already exists",
                    error_code="USER_002",
                    field="email"
                )
        raise DatabaseError(message="Failed to create user")

    except OperationalError as e:
        await db.rollback()
        logger.error(f"Database operational error: {str(e)}", exc_info=True)
        raise DatabaseError(message="Database is currently unavailable")

    except DataError as e:
        db.rollback()
        logger.error(f"Invalid data error: {str(e)}")
        raise ValidationException(
            message="Invalid data format",
            error_code="VAL_001"
        )

    except Exception as e:
        db.rollback()
        logger.error(f"Unexpected error creating user: {str(e)}", exc_info=True)
        raise

Error Response Format

All error responses follow this structure:

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

Data Models and Migrations

Model Definition Best Practices

To ensure Alembic autogenerate works reliably without drift, follow these rules:

1. Simple Indexes: Use Column-Level or __table_args__, Not Both

# ❌ BAD - Creates DUPLICATE indexes with different names
class User(Base):
    role = Column(String(50), index=True)  # Creates ix_users_role

    __table_args__ = (
        Index("ix_user_role", "role"),  # Creates ANOTHER index!
    )

# ✅ GOOD - Choose ONE approach
class User(Base):
    role = Column(String(50))  # No index=True

    __table_args__ = (
        Index("ix_user_role", "role"),  # Single index with explicit name
    )

# ✅ ALSO GOOD - For simple single-column indexes
class User(Base):
    role = Column(String(50), index=True)  # Auto-named ix_users_role

2. Composite Indexes: Always Use __table_args__

class UserOrganization(Base):
    __tablename__ = "user_organizations"

    user_id = Column(UUID, nullable=False)
    organization_id = Column(UUID, nullable=False)
    is_active = Column(Boolean, default=True, nullable=False, index=True)

    __table_args__ = (
        Index("ix_user_org_user_active", "user_id", "is_active"),
        Index("ix_user_org_org_active", "organization_id", "is_active"),
    )

3. Functional/Partial Indexes: Use ix_perf_ Prefix

Alembic cannot auto-detect:

  • Functional indexes: LOWER(column), UPPER(column), expressions
  • Partial indexes: Indexes with WHERE clauses

Solution: Use the ix_perf_ naming prefix. Any index with this prefix is automatically excluded from autogenerate by env.py.

# In migration file (NOT in model) - use ix_perf_ prefix:
op.create_index(
    "ix_perf_users_email_lower",  # <-- ix_perf_ prefix!
    "users",
    [sa.text("LOWER(email)")],  # Functional
    postgresql_where=sa.text("deleted_at IS NULL"),  # Partial
)

No need to update env.py - the prefix convention handles it automatically:

# env.py - already configured:
def include_object(object, name, type_, reflected, compare_to):
    if type_ == "index" and name:
        if name.startswith("ix_perf_"):  # Auto-excluded!
            return False
    return True

To add new performance indexes:

  1. Create a new migration file
  2. Name your indexes with ix_perf_ prefix
  3. Done - Alembic will ignore them automatically

4. Use Correct Types

# ✅ GOOD - PostgreSQL-native types
from sqlalchemy.dialects.postgresql import JSONB, UUID

class User(Base):
    id = Column(UUID(as_uuid=True), primary_key=True)
    preferences = Column(JSONB)  # Not JSON!

# ❌ BAD - Generic types may cause migration drift
from sqlalchemy import JSON
preferences = Column(JSON)  # May detect as different from JSONB

Migration Workflow

Creating Migrations

# Generate autogenerate migration:
python migrate.py generate "Add new field"

# Or inside Docker:
docker exec -w /app backend uv run alembic revision --autogenerate -m "Add new field"

# Apply migration:
python migrate.py apply
# Or: docker exec -w /app backend uv run alembic upgrade head

Testing for Drift

After any model changes, verify no unintended drift:

# Generate test migration
docker exec -w /app backend uv run alembic revision --autogenerate -m "test_drift"

# Check the generated file - should be empty (just 'pass')
# If it has operations, investigate why

# Delete test file
rm backend/app/alembic/versions/*_test_drift.py

Migration File Structure

backend/app/alembic/versions/
├── cbddc8aa6eda_initial_models.py    # Auto-generated, tracks all models
├── 0002_performance_indexes.py        # Manual, functional/partial indexes
└── __init__.py

Summary: What Goes Where

Index Type In Model? Alembic Detects? Where to Define
Simple column (index=True) Yes Yes Column definition
Composite (col1, col2) Yes Yes __table_args__
Unique composite Yes Yes __table_args__ with unique=True
Functional (LOWER(col)) No No Migration with ix_perf_ prefix
Partial (WHERE ...) No No Migration with ix_perf_ prefix

Database Operations

Async CRUD Pattern

IMPORTANT: This application uses async SQLAlchemy with modern patterns for better performance and testability.

Core Principles

  1. Async by Default: All database operations are async
  2. Modern SQLAlchemy 2.0: Use select() instead of .query()
  3. Type Safety: Full type hints with generics
  4. Testability: Easy to mock and test
  5. Consistent Ordering: Always order queries for pagination

Use the Async CRUD Base Class

Always inherit from CRUDBase for database operations:

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.crud.base import CRUDBase
from app.models.user import User
from app.schemas.users import UserCreate, UserUpdate

class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
    """CRUD operations for User model."""

    async def get_by_email(
        self,
        db: AsyncSession,
        email: str
    ) -> User | None:
        """Get user by email address."""
        result = await db.execute(
            select(User).where(User.email == email)
        )
        return result.scalar_one_or_none()

user_crud = CRUDUser(User)

Key Points:

  • Use async def for all methods
  • Use select() instead of db.query()
  • Use await db.execute() for queries
  • Use .scalar_one_or_none() instead of .first()
  • Use T | None instead of Optional[T]

Modern SQLAlchemy Patterns

Query Pattern (Old vs New)

# ❌ OLD - Legacy query() API (sync)
def get_user(db: Session, user_id: UUID) -> Optional[User]:
    return db.query(User).filter(User.id == user_id).first()

# ✅ NEW - Modern select() API (async)
async def get_user(db: AsyncSession, user_id: UUID) -> User | None:
    result = await db.execute(
        select(User).where(User.id == user_id)
    )
    return result.scalar_one_or_none()

Multiple Results

# ❌ OLD
def get_users(db: Session) -> List[User]:
    return db.query(User).all()

# ✅ NEW
async def get_users(db: AsyncSession) -> list[User]:
    result = await db.execute(select(User))
    return list(result.scalars().all())

With Ordering and Pagination

# ✅ CORRECT - Always use ordering for pagination
async def get_users_paginated(
    db: AsyncSession,
    skip: int = 0,
    limit: int = 100
) -> list[User]:
    result = await db.execute(
        select(User)
        .where(User.deleted_at.is_(None))  # Soft delete filter
        .order_by(User.created_at.desc())  # Consistent ordering
        .offset(skip)
        .limit(limit)
    )
    return list(result.scalars().all())

With Relationships (Eager Loading)

from sqlalchemy.orm import selectinload

# Load user with sessions
async def get_user_with_sessions(
    db: AsyncSession,
    user_id: UUID
) -> User | None:
    result = await db.execute(
        select(User)
        .where(User.id == user_id)
        .options(selectinload(User.sessions))  # Eager load relationship
    )
    return result.scalar_one_or_none()

Transaction Management

In Routes (Dependency Injection)

from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db

@router.post("/users", response_model=UserResponse)
async def create_user(
    user_in: UserCreate,
    db: AsyncSession = Depends(get_db)
):
    """
    Create a new user.

    The database session is automatically managed by FastAPI.
    Commit on success, rollback on error.
    """
    return await user_crud.create(db, obj_in=user_in)

Key Points:

  • Route functions must be async def
  • Database parameter is AsyncSession
  • Always await CRUD operations

In Services (Multiple Operations)

async def complex_operation(
    db: AsyncSession,
    user_data: UserCreate,
    session_data: SessionCreate
) -> tuple[User, UserSession]:
    """
    Perform multiple database operations atomically.

    The session automatically commits on success or rolls back on error.
    """
    user = await user_crud.create(db, obj_in=user_data)
    session = await session_crud.create(db, obj_in=session_data)

    # Commit is handled by the route's dependency
    return user, session

Use Soft Deletes

Prefer soft deletes over hard deletes for audit trails:

# Good - Soft delete (sets deleted_at)
await user_crud.soft_delete(db, id=user_id)

# Acceptable only when required - Hard delete
user_crud.remove(db, id=user_id)

Query Patterns

# Always use parameterized queries (SQLAlchemy does this automatically)
# Good
user = db.query(User).filter(User.email == email).first()

# Bad - Never construct raw SQL with string interpolation
db.execute(f"SELECT * FROM users WHERE email = '{email}'")  # SQL INJECTION!

# For complex queries, use SQLAlchemy query builder
from sqlalchemy import and_, or_

active_users = (
    db.query(User)
    .filter(
        and_(
            User.is_active == True,
            User.deleted_at.is_(None),
            or_(
                User.role == "admin",
                User.role == "user"
            )
        )
    )
    .all()
)

API Endpoints

Endpoint Structure

from fastapi import APIRouter, Depends, Request, status
from slowapi import Limiter

router = APIRouter(prefix="/api/v1/users", tags=["users"])
limiter = Limiter(key_func=lambda request: request.client.host)

@router.get(
    "/me",
    response_model=UserResponse,
    summary="Get current user",
    description="Retrieve the currently authenticated user's information.",
    status_code=status.HTTP_200_OK
)
@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 current user information.

    Requires authentication. Rate limited to 30 requests per minute.
    """
    return current_user

Rate Limiting

Apply appropriate rate limits to all endpoints:

# Read operations - More permissive
@limiter.limit("60/minute")

# Write operations - More restrictive
@limiter.limit("10/minute")

# Sensitive operations - Very restrictive
@limiter.limit("5/minute")

# Authentication endpoints - Strict
@limiter.limit("5/minute")

Response Models

Always specify response models:

# Single object
@router.get("/users/{user_id}", response_model=UserResponse)

# List with pagination
@router.get("/users", response_model=PaginatedResponse[UserResponse])

# Message response
@router.delete("/users/{user_id}", response_model=MessageResponse)

# Generic success message
return MessageResponse(success=True, message="User deleted successfully")

Request Validation

Use Pydantic schemas for request validation:

from pydantic import Field, field_validator, ConfigDict

class UserCreate(BaseModel):
    """Schema for creating a new user."""

    model_config = ConfigDict(from_attributes=True)

    email: str = Field(
        ...,
        description="User's email address",
        examples=["user@example.com"]
    )
    password: str = Field(
        ...,
        min_length=8,
        description="Password (minimum 8 characters)",
    )

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        """Validate email format."""
        if not "@" in v:
            raise ValueError("Invalid email format")
        return v.lower()

Pagination

Always implement pagination for list endpoints:

from app.schemas.common import PaginationParams, PaginatedResponse

@router.get("/users", response_model=PaginatedResponse[UserResponse])
def list_users(
    pagination: PaginationParams = Depends(),
    db: Session = Depends(get_db)
):
    """
    List all users with pagination.

    Default page size: 20
    Maximum page size: 100
    """
    users, total = user_crud.get_multi_with_total(
        db,
        skip=pagination.offset,
        limit=pagination.limit
    )
    return PaginatedResponse(data=users, pagination=pagination.create_meta(total))

Authentication & Security

Password Security

from app.core.auth import get_password_hash, verify_password

# Always hash passwords before storing
hashed_password = get_password_hash(plain_password)

# Never log or return passwords
logger.info(f"User {user.email} logged in")  # Good
logger.info(f"Password: {password}")  # NEVER DO THIS!

# Use bcrypt with appropriate cost factor (current: 12)

JWT Tokens

from app.core.auth import create_access_token, create_refresh_token

# Create tokens with appropriate expiry
access_token = create_access_token(
    subject=str(user.id),
    additional_claims={"is_superuser": user.is_superuser}
)

# Always include token type in payload
# Access tokens: 15 minutes
# Refresh tokens: 7 days

Authorization Checks

# Use dependency injection for authorization
from app.api.dependencies.auth import (
    get_current_user,
    get_current_active_user,
    get_current_superuser
)

# Require authentication
@router.get("/protected")
def protected_route(
    current_user: User = Depends(get_current_active_user)
):
    pass

# Require superuser role
@router.post("/admin/users")
def admin_route(
    current_user: User = Depends(get_current_superuser)
):
    pass

# Check ownership
def delete_resource(
    resource_id: UUID,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    resource = resource_crud.get(db, id=resource_id)
    if not resource:
        raise NotFoundError("Resource not found")

    if resource.user_id != current_user.id and not current_user.is_superuser:
        raise AuthorizationError("You can only delete your own resources")

    resource_crud.remove(db, id=resource_id)

Input Validation

# Always validate and sanitize user input
from pydantic import field_validator

class UserUpdate(BaseModel):
    email: Optional[str] = None

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: Optional[str]) -> Optional[str]:
        if v is not None:
            v = v.strip().lower()
            if not "@" in v:
                raise ValueError("Invalid email format")
        return v

Testing

Test Structure

Follow the existing test structure:

tests/
├── conftest.py              # Shared fixtures (async)
├── api/                     # Integration tests
│   ├── test_users.py
│   └── test_auth.py
├── crud/                    # Unit tests for CRUD
├── models/                  # Model tests
└── services/                # Service tests

Async Testing with pytest-asyncio

IMPORTANT: All tests using async database operations must use pytest-asyncio.

import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession

# Mark async tests
@pytest.mark.asyncio
async def test_create_user():
    """Test async user creation."""
    pass

Test Naming Convention

# Test function names should be descriptive and use async
@pytest.mark.asyncio
async def test_create_user_with_valid_data():
    """Test creating a user with valid data succeeds."""
    pass

@pytest.mark.asyncio
async def test_create_user_with_duplicate_email_raises_error():
    """Test creating a user with duplicate email raises DuplicateError."""
    pass

@pytest.mark.asyncio
async def test_get_user_that_does_not_exist_returns_none():
    """Test getting non-existent user returns None."""
    pass

Use Async Fixtures

import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User

@pytest_asyncio.fixture
async def test_user(db_session: AsyncSession) -> User:
    """Create a test user."""
    user = User(
        email="test@example.com",
        hashed_password="hashed",
        is_active=True
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    return user

@pytest.mark.asyncio
async def test_get_user(db_session: AsyncSession, test_user: User):
    """Test retrieving a user by ID."""
    user = await user_crud.get(db_session, id=test_user.id)
    assert user is not None
    assert user.email == test_user.email

Database Test Configuration

Use SQLite in-memory for tests with proper pooling:

# tests/conftest.py
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.models.base import Base

@pytest_asyncio.fixture(scope="function")
async def db_engine():
    """Create async engine for testing."""
    engine = create_async_engine(
        "sqlite+aiosqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,  # IMPORTANT: Share single in-memory DB
    )

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    yield engine

    await engine.dispose()

@pytest_asyncio.fixture
async def db_session(db_engine):
    """Create async session for tests."""
    async_session = sessionmaker(
        db_engine,
        class_=AsyncSession,
        expire_on_commit=False
    )

    async with async_session() as session:
        yield session
        await session.rollback()

Test Coverage

Aim for 80%+ test coverage:

# Test the happy path
@pytest.mark.asyncio
async def test_create_user_success():
    pass

# Test error cases
@pytest.mark.asyncio
async def test_create_user_with_duplicate_email():
    pass

@pytest.mark.asyncio
async def test_create_user_with_invalid_email():
    pass

# Test edge cases
@pytest.mark.asyncio
async def test_create_user_with_empty_password():
    pass

# Test authorization
@pytest.mark.asyncio
async def test_user_cannot_delete_other_users_resources():
    pass

@pytest.mark.asyncio
async def test_superuser_can_delete_any_resource():
    pass

API Testing Pattern (Async)

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

@pytest.mark.asyncio
async def test_create_user_endpoint():
    """Test POST /api/v1/users endpoint (async)."""
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.post(
        "/api/v1/users",
        json={
            "email": "newuser@example.com",
            "password": "securepassword123"
        }
    )

    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "newuser@example.com"
    assert "password" not in data  # Never return password
    assert "id" in data

Logging

Logging Levels

Use appropriate logging levels:

import logging

logger = logging.getLogger(__name__)

# DEBUG - Detailed diagnostic information
logger.debug(f"Processing user {user_id} with parameters: {params}")

# INFO - General informational messages
logger.info(f"User {user_id} logged in successfully")

# WARNING - Warning messages for unexpected but handled situations
logger.warning(f"User {user_id} attempted to access restricted resource")

# ERROR - Error messages for failures
logger.error(f"Failed to create user: {str(e)}", exc_info=True)

# CRITICAL - Critical messages for severe failures
logger.critical(f"Database connection lost: {str(e)}")

What to Log

# DO log:
# - User actions (login, logout, resource access)
# - System events (startup, shutdown, scheduled jobs)
# - Errors and exceptions
# - Performance metrics
# - Security events

logger.info(f"User {user.email} logged in from {ip_address}")
logger.error(f"Failed to send email to {user.email}: {str(e)}", exc_info=True)
logger.warning(f"Rate limit exceeded for IP {ip_address}")

# DON'T log:
# - Passwords or tokens
# - Sensitive personal information
# - Credit card numbers or payment details
# - Full request/response bodies (may contain sensitive data)

# Bad examples:
logger.info(f"User password: {password}")  # NEVER!
logger.debug(f"Token: {access_token}")  # NEVER!

Structured Logging

Use structured logging for better parsing:

logger.info(
    "User action",
    extra={
        "user_id": str(user.id),
        "action": "login",
        "ip_address": request.client.host,
        "user_agent": request.headers.get("user-agent")
    }
)

Documentation

Code Comments

# Use comments to explain WHY, not WHAT
# Good
# Hash password using bcrypt to protect against rainbow table attacks
hashed = get_password_hash(password)

# Bad - The code already shows what it does
# Get password hash
hashed = get_password_hash(password)

# Use comments for complex logic
# Calculate the number of days until token expiration, accounting for
# timezone differences and daylight saving time changes
days_until_expiry = (expires_at - now).total_seconds() / 86400

API Documentation

# Use comprehensive docstrings and FastAPI's automatic documentation

@router.post(
    "/users",
    response_model=UserResponse,
    status_code=status.HTTP_201_CREATED,
    summary="Create a new user",
    description="""
    Create a new user account.

    Requirements:
    - Email must be unique
    - Password must be at least 8 characters
    - No authentication required

    Returns the created user object with a generated UUID.
    """,
    responses={
        201: {"description": "User created successfully"},
        400: {"description": "Invalid input data"},
        409: {"description": "User with email already exists"}
    }
)
def create_user(user_in: UserCreate, db: Session = Depends(get_db)):
    """Create a new user account."""
    pass

Schema Documentation

from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    """
    Schema for creating a new user.

    This schema is used for user registration and admin user creation.
    Passwords are automatically hashed before storage.
    """

    email: str = Field(
        ...,
        description="User's email address (must be unique)",
        examples=["user@example.com"],
        json_schema_extra={"format": "email"}
    )

    password: str = Field(
        ...,
        min_length=8,
        description="User's password (minimum 8 characters)",
        examples=["SecurePass123!"]
    )

    is_active: bool = Field(
        default=True,
        description="Whether the user account is active"
    )

README Files

Each major feature or module should have a README explaining:

  • Purpose and overview
  • Architecture and design decisions
  • Setup and configuration
  • Usage examples
  • API endpoints (if applicable)
  • Testing instructions

Example: backend/SESSION_IMPLEMENTATION_CONTEXT.md

Summary

Following these coding standards ensures:

  • Consistency: Code is uniform across the project
  • Maintainability: Easy to understand and modify
  • Security: Best practices prevent vulnerabilities
  • Quality: High test coverage and error handling
  • Documentation: Clear and comprehensive docs

For feature implementation examples, see:

  • Architecture Guide: backend/docs/ARCHITECTURE.md
  • Feature Example: backend/docs/FEATURE_EXAMPLE.md