- Removed outdated and redundant Alembic migration files to streamline the migration directory. This improves maintainability and eliminates duplicate or unused scripts.
30 KiB
Coding Standards
This document outlines the coding standards and best practices for the FastAPI backend application.
Table of Contents
- General Principles
- Code Organization
- Naming Conventions
- Error Handling
- Data Models and Migrations
- Database Operations
- API Endpoints
- Authentication & Security
- Testing
- Logging
- Documentation
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 ofList[T](Python 3.10+) - Use
dict[K, V]instead ofDict[K, V] - Use
T | Noneinstead ofOptional[T] - Use
str | intinstead ofUnion[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
WHEREclauses
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:
- Create a new migration file
- Name your indexes with
ix_perf_prefix - 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
- Async by Default: All database operations are async
- Modern SQLAlchemy 2.0: Use
select()instead of.query() - Type Safety: Full type hints with generics
- Testability: Easy to mock and test
- 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 deffor all methods - Use
select()instead ofdb.query() - Use
await db.execute()for queries - Use
.scalar_one_or_none()instead of.first() - Use
T | Noneinstead ofOptional[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
awaitCRUD 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