Files
oicd/docs/guides/python-quick-start-guide.md
2025-11-30 00:07:24 +01:00

1265 lines
32 KiB
Markdown

# Python Developer Quick Start Guide
## FastAPI & Flask - Essential Patterns for Daily Development
**This is your ONE reference document for Python API development.**
Everything you need to write clean, production-ready code.
---
## Table of Contents
1. [Project Setup](#project-setup)
2. [Project Structure](#project-structure)
3. [The Four Layers](#the-four-layers)
4. [Code Standards](#code-standards)
5. [Module Size Guidelines](#module-size-guidelines)
6. [Database Patterns](#database-patterns)
7. [Error Handling](#error-handling)
8. [Logging](#logging)
9. [Testing](#testing)
10. [Security Essentials](#security-essentials)
11. [Daily Checklist](#daily-checklist)
---
## Project Setup
### Initial Setup
```bash
# Create project
mkdir my-api && cd my-api
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# Install dependencies
pip install fastapi uvicorn sqlalchemy pydantic-settings
pip install pytest pytest-cov black ruff mypy --dev
# Create requirements files
pip freeze > requirements.txt
```
### Essential Configuration Files
#### `pyproject.toml`
```toml
[tool.black]
line-length = 100
target-version = ['py311']
[tool.ruff]
line-length = 100
select = ["E", "W", "F", "I", "B", "C4", "UP"]
[tool.mypy]
python_version = "3.11"
warn_return_any = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=app --cov-report=html"
```
#### `.env.example`
```bash
# Application
ENVIRONMENT=development
LOG_LEVEL=INFO
SECRET_KEY=your-secret-key-change-in-production
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# External Services
REDIS_URL=redis://localhost:6379/0
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
```
---
## Project Structure
### FastAPI Structure (Recommended)
```
my-api/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app initialization
│ ├── config.py # Settings (Pydantic)
│ ├── dependencies.py # Dependency injection
│ │
│ ├── api/
│ │ └── v1/
│ │ ├── endpoints/
│ │ │ ├── users.py # Thin endpoints
│ │ │ └── auth.py
│ │ └── router.py
│ │
│ ├── services/ # Business logic HERE
│ │ ├── user_service.py
│ │ └── auth_service.py
│ │
│ ├── repositories/ # Database operations HERE
│ │ ├── base_repository.py
│ │ └── user_repository.py
│ │
│ ├── models/ # SQLAlchemy models
│ │ └── user.py
│ │
│ ├── schemas/ # Pydantic schemas
│ │ └── user.py
│ │
│ ├── core/
│ │ ├── database.py
│ │ ├── security.py
│ │ └── logging_config.py
│ │
│ ├── exceptions.py
│ └── utils/
│
├── tests/
│ ├── conftest.py
│ ├── test_api/
│ └── test_services/
│
├── .env
├── .env.example
├── .gitignore
├── pyproject.toml
├── requirements.txt
└── README.md
```
---
## The Four Layers
**CRITICAL:** Always follow this pattern to avoid spaghetti code.
```
┌─────────────────────────────────────┐
│ 1. API LAYER (endpoints) │ ← Thin controllers (5-10 lines)
├─────────────────────────────────────┤
│ 2. SERVICE LAYER │ ← Business logic lives HERE
├─────────────────────────────────────┤
│ 3. REPOSITORY LAYER │ ← Database operations only
├─────────────────────────────────────┤
│ 4. MODEL LAYER │ ← Data structures
└─────────────────────────────────────┘
```
### 1. API Layer (Thin Endpoints)
**Rule:** Endpoints should be < 10 lines. Just call service layer.
```python
# app/api/v1/endpoints/users.py
from fastapi import APIRouter, Depends, status
from typing import Annotated
from app.schemas.user import UserCreate, UserResponse
from app.services.user_service import UserService
from app.dependencies import get_user_service
router = APIRouter()
@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_in: UserCreate,
service: Annotated[UserService, Depends(get_user_service)],
) -> UserResponse:
"""Create new user - endpoint is THIN."""
return await service.register_user(user_in)
@router.get("/users/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
service: Annotated[UserService, Depends(get_user_service)],
) -> UserResponse:
"""Get user by ID."""
return await service.get_user(user_id)
```
**Key Points:**
- ✅ No business logic in endpoints
- ✅ No database calls in endpoints
- ✅ Just parse input → call service → return response
---
### 2. Service Layer (Business Logic)
**Rule:** ALL business logic goes here. Services orchestrate workflows.
```python
# app/services/user_service.py
from sqlalchemy.orm import Session
from loguru import logger
from app.repositories.user_repository import UserRepository
from app.schemas.user import UserCreate
from app.models.user import User
from app.exceptions import ConflictError, NotFoundError
from app.core.security import get_password_hash
class UserService:
"""User service - contains ALL business logic for users."""
def __init__(
self,
user_repo: UserRepository,
email_service: EmailService,
db: Session,
):
self.user_repo = user_repo
self.email_service = email_service
self.db = db
async def register_user(self, user_data: UserCreate) -> User:
"""
Register a new user - complete workflow.
Business Rules:
1. Email must be unique
2. Password must be hashed
3. Send welcome email
4. User starts inactive until email verified
"""
logger.info("Registering user", extra={"email": user_data.email})
# Rule 1: Check email uniqueness
existing = await self.user_repo.get_by_email(user_data.email)
if existing:
raise ConflictError("Email already registered")
# Rule 2: Hash password
hashed_password = get_password_hash(user_data.password)
# Create user
try:
user = await self.user_repo.create(
email=user_data.email,
username=user_data.username,
hashed_password=hashed_password,
is_active=False, # Rule 4
)
# Rule 3: Send email
await self.email_service.send_welcome_email(user.email)
self.db.commit()
logger.info("User registered", extra={"user_id": user.id})
return user
except Exception as e:
self.db.rollback()
logger.error("Registration failed", exc_info=True)
raise
async def get_user(self, user_id: int) -> User:
"""Get user by ID with business logic."""
user = await self.user_repo.get_by_id(user_id)
if not user:
raise NotFoundError("User", user_id)
# Business rule: Don't return deleted users
if user.is_deleted:
raise NotFoundError("User", user_id)
return user
```
**Key Points:**
- ✅ Business rules are explicit
- ✅ Orchestrates multiple operations
- ✅ Handles transactions
- ✅ Logged properly
---
### 3. Repository Layer (Data Access)
**Rule:** Only database operations. No business logic.
```python
# app/repositories/user_repository.py
from typing import Optional, List
from sqlalchemy.orm import Session
from app.models.user import User
class UserRepository:
"""User repository - handles ALL database operations for users."""
def __init__(self, db: Session):
self.db = db
async def get_by_id(self, user_id: int) -> Optional[User]:
"""Get user by ID."""
return self.db.query(User).filter(User.id == user_id).first()
async def get_by_email(self, email: str) -> Optional[User]:
"""Get user by email."""
return self.db.query(User).filter(User.email == email).first()
async def get_all(self, skip: int = 0, limit: int = 100) -> List[User]:
"""Get all users with pagination."""
return self.db.query(User).offset(skip).limit(limit).all()
async def create(
self,
email: str,
username: str,
hashed_password: str,
is_active: bool = True,
) -> User:
"""Create new user."""
user = User(
email=email,
username=username,
hashed_password=hashed_password,
is_active=is_active,
)
self.db.add(user)
self.db.flush() # Get ID without committing
return user
async def update(self, user: User, **kwargs) -> User:
"""Update user."""
for key, value in kwargs.items():
setattr(user, key, value)
self.db.flush()
return user
async def delete(self, user: User) -> None:
"""Delete user."""
self.db.delete(user)
self.db.flush()
```
**Key Points:**
- ✅ Pure data access
- ✅ No business logic
- ✅ Easy to test with mocks
---
### 4. Model Layer (Domain Models)
**Rule:** Data structure + simple helper methods only.
```python
# app/models/user.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from datetime import datetime
from app.core.database import Base
class User(Base):
"""User domain model."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, index=True, nullable=False)
username = Column(String(100), unique=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
is_deleted = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
def __repr__(self) -> str:
return f"<User {self.username}>"
# Simple domain methods are OK
def can_login(self) -> bool:
"""Check if user can login."""
return self.is_active and not self.is_deleted
```
---
### Dependency Injection
```python
# app/dependencies.py
from typing import Annotated, Generator
from fastapi import Depends
from sqlalchemy.orm import Session
from app.core.database import SessionLocal
from app.repositories.user_repository import UserRepository
from app.services.user_service import UserService
from app.services.email_service import EmailService
def get_db() -> Generator[Session, None, None]:
"""Get database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_user_repository(
db: Annotated[Session, Depends(get_db)]
) -> UserRepository:
"""Get user repository."""
return UserRepository(db)
def get_email_service() -> EmailService:
"""Get email service."""
return EmailService()
def get_user_service(
user_repo: Annotated[UserRepository, Depends(get_user_repository)],
email_service: Annotated[EmailService, Depends(get_email_service)],
db: Annotated[Session, Depends(get_db)],
) -> UserService:
"""Get user service with all dependencies."""
return UserService(
user_repo=user_repo,
email_service=email_service,
db=db,
)
```
---
## Code Standards
### Type Hints (MANDATORY)
```python
# BAD: No type hints
def get_user(user_id):
return db.query(User).get(user_id)
# GOOD: Clear type hints
def get_user(user_id: int) -> Optional[User]:
return db.query(User).filter(User.id == user_id).first()
# Modern Python 3.10+ syntax
def get_users(limit: int = 10) -> list[User]:
return db.query(User).limit(limit).all()
```
### Pydantic Schemas
```python
# app/schemas/user.py
from pydantic import BaseModel, EmailStr, Field, ConfigDict
from datetime import datetime
class UserBase(BaseModel):
email: EmailStr
username: str = Field(..., min_length=3, max_length=50)
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
email: EmailStr | None = None
username: str | None = None
class UserResponse(UserBase):
id: int
is_active: bool
created_at: datetime
model_config = ConfigDict(from_attributes=True)
```
### Naming Conventions
```python
# Constants
MAX_CONNECTIONS = 100
API_BASE_URL = "https://api.example.com"
# Classes
class UserService:
pass
# Functions and variables
def get_user_by_email(email: str) -> User:
pass
# Private methods
def _internal_helper():
pass
# Boolean variables
is_active = True
has_permission = False
should_retry = True
```
---
## Module Size Guidelines
### Maximum Lines Per Module
**Keep modules focused and manageable:**
| Module Type | Ideal Size | Maximum | Action If Exceeds |
|-------------|-----------|---------|-------------------|
| **Endpoints** | 200-250 lines | 300 lines | Split by resource/feature |
| **Services** | 300-400 lines | 500 lines | Split by subdomain |
| **Repositories** | 200-300 lines | 400 lines | Use base repository pattern |
| **Models** | 150-200 lines | 300 lines | One model per file |
| **Utils** | 100-150 lines | 200 lines | Split by function category |
**General Rule: Never exceed 1000 lines in a single module.**
### When to Split a Module
#### Warning Signs:
- Hard to find specific functions
- Constant scrolling up and down
- Multiple unrelated responsibilities
- Test file is becoming huge
- Frequent merge conflicts
#### Example: Splitting Large Service
```python
# BAD: One giant service (1200 lines)
# app/services/user_service.py
class UserService:
def register_user(): pass # 80 lines
def login(): pass # 60 lines
def logout(): pass # 40 lines
def reset_password(): pass # 70 lines
def update_profile(): pass # 50 lines
def upload_avatar(): pass # 80 lines
def send_notification(): pass # 60 lines
def export_data(): pass # 90 lines
# ... 15 more methods (1200 lines total) ❌
# GOOD: Split into focused services
# app/services/user_service.py (400 lines)
class UserService:
def register_user(): pass
def get_user(): pass
def update_user(): pass
def delete_user(): pass
# Core CRUD operations ✅
# app/services/user_auth_service.py (300 lines)
class UserAuthService:
def login(): pass
def logout(): pass
def reset_password(): pass
def verify_email(): pass
# Authentication logic ✅
# app/services/user_profile_service.py (250 lines)
class UserProfileService:
def update_profile(): pass
def upload_avatar(): pass
def get_statistics(): pass
# Profile management ✅
# app/services/user_notification_service.py (200 lines)
class UserNotificationService:
def send_welcome_email(): pass
def send_notification(): pass
# Notifications ✅
```
### Function Size Guidelines
```python
# Functions should be short and focused
# Ideal: 10-30 lines
# Maximum: 50 lines
# BAD: Function too long (100+ lines)
def process_order(order_data):
# 100 lines of logic
pass # ❌
# GOOD: Break into smaller functions
def process_order(order_data):
"""Process order - orchestrates workflow."""
validate_order(order_data) # 15 lines
calculate_total(order_data) # 20 lines
process_payment(order_data) # 25 lines
send_confirmation(order_data) # 15 lines
# Each function is focused and testable ✅
```
---
## Database Patterns
### Database Setup
```python
# app/core/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from app.core.config import settings
engine = create_engine(
settings.DATABASE_URL,
pool_pre_ping=True,
pool_size=10,
max_overflow=20,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
```
### Models with Relationships
```python
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False)
# Relationships
posts = relationship("Post", back_populates="author", cascade="all, delete-orphan")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String, nullable=False)
author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relationships
author = relationship("User", back_populates="posts")
```
### Query Optimization
```python
# BAD: N+1 query problem
users = db.query(User).all()
for user in users:
print(user.posts) # Separate query for each user!
# GOOD: Eager loading
from sqlalchemy.orm import joinedload
users = db.query(User).options(joinedload(User.posts)).all()
for user in users:
print(user.posts) # No additional queries
```
---
## Error Handling
### Custom Exception Hierarchy
```python
# app/exceptions.py
from typing import Optional, Any
class AppException(Exception):
"""Base application exception."""
def __init__(
self,
message: str,
error_code: str,
status_code: int = 500,
details: dict[str, Any] | None = None,
):
self.message = message
self.error_code = error_code
self.status_code = status_code
self.details = details or {}
super().__init__(self.message)
class ValidationError(AppException):
def __init__(self, message: str, field: str | None = None):
super().__init__(
message=message,
error_code="VALIDATION_ERROR",
status_code=400,
details={"field": field} if field else {},
)
class NotFoundError(AppException):
def __init__(self, resource: str, identifier: Any):
super().__init__(
message=f"{resource} not found",
error_code="NOT_FOUND",
status_code=404,
details={"resource": resource, "identifier": str(identifier)},
)
class ConflictError(AppException):
def __init__(self, message: str):
super().__init__(
message=message,
error_code="CONFLICT",
status_code=409,
)
class AuthenticationError(AppException):
def __init__(self, message: str = "Authentication failed"):
super().__init__(
message=message,
error_code="AUTHENTICATION_ERROR",
status_code=401,
)
```
### Exception Handlers (FastAPI)
```python
# app/main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from loguru import logger
from app.exceptions import AppException
app = FastAPI()
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
"""Handle custom application exceptions."""
# Log based on severity
if exc.status_code >= 500:
logger.error(f"Server error: {exc.message}", extra={"error_code": exc.error_code}, exc_info=True)
else:
logger.warning(f"Client error: {exc.message}", extra={"error_code": exc.error_code})
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.error_code,
"message": exc.message,
"details": exc.details,
}
},
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Catch-all exception handler."""
logger.critical("Unhandled exception", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred",
"details": {},
}
},
)
```
---
## Logging
### Setup (Loguru - Recommended)
```python
# app/core/logging_config.py
import sys
from loguru import logger
from app.core.config import settings
def setup_logging() -> None:
"""Configure application logging."""
logger.remove()
# Console logging
if settings.ENVIRONMENT == "development":
logger.add(
sys.stdout,
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}:{function}:{line}</cyan> | <level>{message}</level>",
level=settings.LOG_LEVEL,
colorize=True,
)
else:
# JSON logging for production
logger.add(
sys.stdout,
level=settings.LOG_LEVEL,
serialize=True,
)
# File logging
logger.add(
"logs/app_{time:YYYY-MM-DD}.log",
rotation="00:00",
retention="30 days",
level=settings.LOG_LEVEL,
serialize=True,
)
```
### Request Logging Middleware
```python
# app/middleware/logging.py
import time
import uuid
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from loguru import logger
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request_id = str(uuid.uuid4())
request.state.request_id = request_id
start_time = time.time()
logger.info(
"Incoming request",
extra={
"request_id": request_id,
"method": request.method,
"path": request.url.path,
}
)
response = await call_next(request)
duration = time.time() - start_time
logger.info(
"Request completed",
extra={
"request_id": request_id,
"status_code": response.status_code,
"duration_ms": round(duration * 1000, 2),
}
)
response.headers["X-Request-ID"] = request_id
return response
# Add to app
app.add_middleware(RequestLoggingMiddleware)
```
### Log Levels Usage
```python
# DEBUG - Detailed diagnostic info
logger.debug("Database query executed", extra={"query": sql, "duration_ms": 45.2})
# INFO - Important business events
logger.info("User registered", extra={"user_id": user.id})
# WARNING - Unexpected but handled
logger.warning("API rate limit approaching", extra={"requests": 950, "limit": 1000})
# ERROR - Operation failed but app continues
logger.error("Failed to send email", extra={"user_id": user.id}, exc_info=True)
# CRITICAL - System failure
logger.critical("Database connection pool exhausted", extra={"pool_size": 10})
```
---
## Testing
### Test Setup
```python
# tests/conftest.py
import pytest
from typing import Generator
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.core.database import Base
from app.dependencies import get_db
# Test database
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def db() -> Generator:
"""Create test database."""
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db) -> Generator:
"""Create test client."""
def override_get_db():
try:
yield db
finally:
pass
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
```
### Testing Services (Unit Tests)
```python
# tests/test_services/test_user_service.py
import pytest
from unittest.mock import Mock, AsyncMock
from app.services.user_service import UserService
from app.schemas.user import UserCreate
from app.exceptions import ConflictError
@pytest.fixture
def mock_user_repo():
return Mock()
@pytest.fixture
def mock_email_service():
return Mock()
@pytest.fixture
def user_service(mock_user_repo, mock_email_service):
return UserService(
user_repo=mock_user_repo,
email_service=mock_email_service,
db=Mock(),
)
async def test_register_user_success(user_service, mock_user_repo):
"""Test successful user registration."""
# Arrange
mock_user_repo.get_by_email = AsyncMock(return_value=None)
mock_user_repo.create = AsyncMock(return_value=Mock(id=1, email="test@example.com"))
user_data = UserCreate(email="test@example.com", username="test", password="pass123")
# Act
result = await user_service.register_user(user_data)
# Assert
assert result.id == 1
mock_user_repo.create.assert_called_once()
async def test_register_user_email_exists(user_service, mock_user_repo):
"""Test registration with existing email."""
# Arrange
mock_user_repo.get_by_email = AsyncMock(return_value=Mock(id=1))
user_data = UserCreate(email="test@example.com", username="test", password="pass123")
# Act & Assert
with pytest.raises(ConflictError):
await user_service.register_user(user_data)
```
### Testing API Endpoints
```python
# tests/test_api/test_users.py
def test_create_user(client):
"""Test POST /api/v1/users."""
response = client.post(
"/api/v1/users",
json={
"email": "test@example.com",
"username": "testuser",
"password": "password123",
},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert "password" not in data
def test_get_user(client):
"""Test GET /api/v1/users/{id}."""
# Create user first
create_response = client.post("/api/v1/users", json={...})
user_id = create_response.json()["id"]
# Get user
response = client.get(f"/api/v1/users/{user_id}")
assert response.status_code == 200
assert response.json()["id"] == user_id
```
---
## Security Essentials
### Password Hashing
```python
# app/core/security.py
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password: str) -> str:
"""Hash a password."""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password."""
return pwd_context.verify(plain_password, hashed_password)
```
### JWT Authentication
```python
from datetime import datetime, timedelta
import jwt
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
def create_access_token(user_id: int) -> str:
"""Create JWT access token."""
expire = datetime.utcnow() + timedelta(minutes=30)
to_encode = {"sub": str(user_id), "exp": expire}
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def decode_access_token(token: str) -> dict | None:
"""Decode JWT token."""
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.PyJWTError:
return None
```
### Input Validation
```python
from pydantic import validator
class UserCreate(BaseModel):
email: EmailStr
password: str
@validator("password")
def password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if not any(char.isdigit() for char in v):
raise ValueError("Password must contain a digit")
return v
```
### SQL Injection Prevention
```python
# BAD: Never do this
query = f"SELECT * FROM users WHERE email = '{email}'"
# GOOD: Use ORM
user = db.query(User).filter(User.email == email).first()
# GOOD: Parameterized queries
from sqlalchemy import text
query = text("SELECT * FROM users WHERE email = :email")
db.execute(query, {"email": email})
```
---
## Daily Checklist
### Before Writing Code
- [ ] Understand which layer you're working in
- [ ] Business logic goes in SERVICE layer
- [ ] Database operations go in REPOSITORY layer
- [ ] Endpoints stay THIN (< 10 lines)
### While Writing Code
- [ ] Add type hints to all functions
- [ ] Log important operations
- [ ] Handle errors with custom exceptions
- [ ] Validate all inputs with Pydantic
- [ ] Use dependency injection
- [ ] Keep functions under 50 lines
- [ ] Keep modules under 500 lines
### Before Committing
- [ ] Run tests: `pytest`
- [ ] Format code: `black .`
- [ ] Lint code: `ruff check .`
- [ ] Type check: `mypy app/`
- [ ] No hardcoded secrets
- [ ] No print() or console.log()
### Code Review Checklist
- [ ] Endpoints are thin (just call service)
- [ ] Business logic is in services
- [ ] Database queries are in repositories
- [ ] All functions have type hints
- [ ] Functions are under 50 lines
- [ ] Modules are under 500 lines (split if larger)
- [ ] Errors are logged properly
- [ ] Tests are included
- [ ] No security vulnerabilities
---
## Quick Reference Commands
```bash
# Development
uvicorn app.main:app --reload
# Testing
pytest
pytest --cov=app --cov-report=html
# Code Quality
black .
ruff check . --fix
mypy app/
# Database
alembic revision --autogenerate -m "message"
alembic upgrade head
```
---
## Common Mistakes to Avoid
### ❌ DON'T: Put business logic in endpoints
```python
@router.post("/users")
async def create_user(user_in: UserCreate, db: Session = Depends(get_db)):
# Checking email exists - BUSINESS LOGIC!
if db.query(User).filter(User.email == user_in.email).first():
raise HTTPException(400, "Email exists")
# Creating user - DATABASE OPERATION!
user = User(**user_in.dict())
db.add(user)
db.commit()
return user # WRONG!
```
### ✅ DO: Thin endpoint, call service
```python
@router.post("/users")
async def create_user(
user_in: UserCreate,
service: Annotated[UserService, Depends(get_user_service)],
):
return await service.register_user(user_in)
```
---
### ❌ DON'T: Skip type hints
```python
def get_user(user_id): # What type? What returns?
return db.query(User).get(user_id)
```
### ✅ DO: Always use type hints
```python
def get_user(user_id: int) -> Optional[User]:
return db.query(User).filter(User.id == user_id).first()
```
---
### ❌ DON'T: Ignore errors
```python
try:
result = risky_operation()
except:
pass # Silent failure!
```
### ✅ DO: Handle and log errors
```python
try:
result = risky_operation()
except SpecificError as e:
logger.error("Operation failed", exc_info=True)
raise AppException("Failed to process", error_code="OPERATION_FAILED")
```
---
## Summary
### The Golden Rules:
1. **Four Layers Always:**
- API → Service → Repository → Model
2. **Thin Endpoints:**
- Just call service layer (< 10 lines)
3. **Business Logic in Services:**
- All orchestration, rules, workflows
4. **Type Hints Everywhere:**
- Every function signature
5. **Log Everything Important:**
- With context and correlation IDs
6. **Test Each Layer:**
- Unit tests for services
- Integration tests for APIs
7. **Security First:**
- Hash passwords, validate inputs, prevent SQL injection
---
**This guide covers 90% of daily Python API development. Keep it open while coding!** 🚀