Development Guide
Development Guide
This guide covers everything you need to know about extending, customizing, and developing with the FastAPI boilerplate.
Extending the Boilerplate
Creating Custom Middleware
Create middleware in src/app/middleware/:
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Pre-processing
start_time = time.time()
# Process request
response = await call_next(request)
# Post-processing
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return responseRegister in src/app/main.py:
from .middleware.custom_header_middleware import CustomHeaderMiddleware
app.add_middleware(CustomHeaderMiddleware)Testing
Test Configuration
The boilerplate uses pytest for testing. Test configuration is in pytest.ini and test dependencies in pyproject.toml.
Writing Tests
Model Tests
# tests/test_models.py
import pytest
from src.app.models.user import User
@pytest_asyncio.fixture
async def test_user(async_session):
user = User(name="Test User", username="testuser", email="test@example.com", hashed_password="hashed_password")
async_session.add(user)
await async_session.commit()
await async_session.refresh(user)
return user
async def test_user_creation(test_user):
assert test_user.name == "Test User"
assert test_user.username == "testuser"
assert test_user.email == "test@example.com"API Endpoint Tests
# tests/test_api.py
import pytest
from httpx import AsyncClient
async def test_create_user(async_client: AsyncClient):
user_data = {"name": "New User", "username": "newuser", "email": "new@example.com", "password": "SecurePass123!"}
response = await async_client.post("/services/v1/users", json=user_data)
assert response.status_code == 201
data = response.json()
assert data["name"] == "New User"
assert data["username"] == "newuser"
assert "hashed_password" not in data # Ensure password not exposed
async def test_read_users(async_client: AsyncClient):
response = await async_client.get("/services/v1/users")
assert response.status_code == 200
data = response.json()
assert "data" in data
assert "total_count" in dataCRUD Tests
# tests/test_crud.py
import pytest
from src.app.crud.crud_users import crud_users
from src.app.schemas.user import UserCreate
async def test_crud_create_user(async_session):
user_data = UserCreate(name="CRUD User", username="cruduser", email="crud@example.com", password="password123")
user = await crud_users.create(db=async_session, object=user_data)
assert user["name"] == "CRUD User"
assert user["username"] == "cruduser"
async def test_crud_get_user(async_session, test_user):
retrieved_user = await crud_users.get(db=async_session, id=test_user.id)
assert retrieved_user["name"] == test_user.nameRunning Tests
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov=src
# Run specific test file
uv run pytest tests/test_api.py
# Run with verbose output
uv run pytest -v
# Run tests matching pattern
uv run pytest -k "test_user"Customization
Environment-Specific Configuration
Create environment-specific settings:
# src/app/core/config.py
class LocalSettings(Settings):
ENVIRONMENT: str = "local"
DEBUG: bool = True
class ProductionSettings(Settings):
ENVIRONMENT: str = "production"
DEBUG: bool = False
# Production-specific settings
def get_settings():
env = os.getenv("ENVIRONMENT", "local")
if env == "production":
return ProductionSettings()
return LocalSettings()
settings = get_settings()Custom Logging
Configure logging in src/app/core/config.py:
import logging
from pythonjsonlogger import jsonlogger
def setup_logging():
# JSON logging for production
if settings.ENVIRONMENT == "production":
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(logHandler)
logger.setLevel(logging.INFO)
else:
# Simple logging for development
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")Opting Out of Services
Disabling Redis Caching
- Remove cache decorators from endpoints
- Update dependencies in
src/app/core/config.py:
class Settings(BaseSettings):
# Comment out or remove Redis cache settings
# REDIS_CACHE_HOST: str = "localhost"
# REDIS_CACHE_PORT: int = 6379
pass- Remove Redis cache imports and usage
Disabling Background Tasks (ARQ)
- Remove ARQ from
pyproject.tomldependencies - Remove worker configuration from
docker-compose.yml - Delete
src/app/core/worker/directory - Remove task-related endpoints
Disabling Rate Limiting
- Remove rate limiting dependencies from endpoints:
# Remove this dependency
dependencies = [Depends(rate_limiter_dependency)]- Remove rate limiting models and schemas
- Update database migrations to remove rate limit tables
Disabling Authentication
- Remove JWT dependencies from protected endpoints
- Remove user-related models and endpoints
- Update database to remove user tables
- Remove authentication middleware
Minimal FastAPI Setup
For a minimal setup with just basic FastAPI:
# src/app/main.py (minimal version)
from fastapi import FastAPI
app = FastAPI(title="Minimal API", description="Basic FastAPI application", version="1.0.0")
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/health")
async def health_check():
return {"status": "healthy"}Best Practices
Code Organization
- Keep models, schemas, and CRUD operations in separate files
- Use consistent naming conventions across the application
- Group related functionality in modules
- Follow FastAPI and Pydantic best practices
Database Operations
- Always use transactions for multi-step operations
- Implement soft deletes for important data
- Use database constraints for data integrity
- Index frequently queried columns
API Design
- Use consistent response formats
- Implement proper error handling
- Version your APIs from the start
- Document all endpoints with proper schemas
Security
- Never expose sensitive data in API responses
- Use proper authentication and authorization
- Validate all input data
- Implement rate limiting for public endpoints
- Use HTTPS in production
Performance
- Use async/await consistently
- Implement caching for expensive operations
- Use database connection pooling
- Monitor and optimize slow queries
- Use pagination for large datasets
Troubleshooting
Common Issues
Import Errors: Ensure all new models are imported in __init__.py files
Migration Failures: Check model definitions and relationships before generating migrations
Test Failures: Verify test database configuration and isolation
Performance Issues: Check for N+1 queries and missing database indexes
Authentication Problems: Verify JWT configuration and token expiration settings
Debugging Tips
- Use FastAPI's automatic interactive docs at
/docs - Enable SQL query logging in development
- Use proper logging throughout the application
- Test endpoints with realistic data volumes
- Monitor database performance with query analysis