FastAPI is great for quick prototypes, but production apps need more structure. Here are patterns that scale.
Project Structure
myapp/
app/
__init__.py
main.py
config.py
dependencies.py
routers/
__init__.py
users.py
orders.py
models/
__init__.py
user.py
order.py
schemas/
__init__.py
user.py
order.py
services/
__init__.py
user_service.py
Configuration with Pydantic Settings
# app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
database_url: str
redis_url: str = "redis://localhost:6379"
secret_key: str
debug: bool = False
allowed_origins: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
@lru_cache
def get_settings() -> Settings:
return Settings()
Router Organization
# app/routers/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from app.schemas.user import UserCreate, UserResponse
from app.services.user_service import UserService
from app.dependencies import get_user_service
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate, service: UserService = Depends(get_user_service)):
existing = await service.get_by_email(user.email)
if existing:
raise HTTPException(status_code=409, detail="Email already registered")
return await service.create(user)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, service: UserService = Depends(get_user_service)):
user = await service.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Dependency Injection
# app/dependencies.py
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.services.user_service import UserService
async def get_user_service(db: AsyncSession = Depends(get_db)) -> UserService:
return UserService(db)
Error Handling
# app/main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class AppException(Exception):
def __init__(self, status_code: int, detail: str):
self.status_code = status_code
self.detail = detail
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
return JSONResponse(status_code=exc.status_code, content={"error": exc.detail})
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
return JSONResponse(status_code=500, content={"error": "Internal server error"})
Middleware Stack
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import time
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def add_timing_header(request: Request, call_next):
start = time.time()
response = await call_next(request)
response.headers["X-Process-Time"] = str(time.time() - start)
return response
Background Tasks
from fastapi import BackgroundTasks
async def send_welcome_email(email: str):
# slow email sending logic
pass
@router.post("/register")
async def register(user: UserCreate, bg: BackgroundTasks):
new_user = await service.create(user)
bg.add_task(send_welcome_email, user.email)
return new_user # returns immediately
Key Takeaways
- Separate routers, schemas, models, and services
- Use Pydantic Settings for configuration
- Dependency injection keeps code testable
- Custom exception handlers for consistent error responses
- Background tasks for non-blocking operations
6. Always add CORS middleware for frontend integration
🚀 Level up your AI workflow! Check out my AI Developer Mega Prompt Pack — 80 battle-tested prompts for developers. $9.99
Top comments (0)