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

1214 lines
30 KiB
Markdown

# Coding Standards
This document outlines the coding standards and best practices for the FastAPI backend application.
## Table of Contents
- [General Principles](#general-principles)
- [Code Organization](#code-organization)
- [Naming Conventions](#naming-conventions)
- [Error Handling](#error-handling)
- [Data Models and Migrations](#data-models-and-migrations)
- [Database Operations](#database-operations)
- [API Endpoints](#api-endpoints)
- [Authentication & Security](#authentication--security)
- [Testing](#testing)
- [Logging](#logging)
- [Documentation](#documentation)
## General Principles
### 1. Follow PEP 8
All Python code should follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) 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:
```python
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:
```python
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:
```bash
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
```python
# 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
```python
# 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
```python
# Use PascalCase for class names
class UserSession:
pass
class AuthenticationError(APIException):
pass
```
### Database Tables and Columns
```python
# 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
```python
# Use kebab-case for URL paths
@router.get("/user-sessions")
@router.post("/password-reset")
@router.delete("/user-sessions/expired")
```
### Files and Directories
```python
# 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`:
```python
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):
```python
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:
```python
{
"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
```python
# ❌ 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__`
```python
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`.
```python
# 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:
```python
# 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
```python
# ✅ 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
```bash
# 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:
```bash
# 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:
```python
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)
```python
# ❌ 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
```python
# ❌ 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
```python
# ✅ 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)
```python
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)
```python
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)
```python
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:
```python
# 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
```python
# 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
```python
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:
```python
# 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:
```python
# 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:
```python
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:
```python
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
```python
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
```python
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
```python
# 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
```python
# 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`.
```python
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
```python
# 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
```python
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:
```python
# 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:
```python
# 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)
```python
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:
```python
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
```python
# 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:
```python
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
```python
# 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
```python
# 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
```python
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`