7 FastAPI Tips That Saved Me Hours of Debugging
7 FastAPI Tips That Saved Me Hours of Debugging Practical tricks I wish I knew before building my first FastAPI backend. I've been building APIs with FastAPI for over two years. Here are the tips that genuinely saved me from debugging headaches — especially the ones that aren't obvious from the docs. response_model_exclude_unset for Partial Updates When building a PATCH endpoint, you want to update only the fields the client actually sent: from fastapi import FastAPI from pydantic import BaseModel from typing import Optional app = FastAPI() class UserUpdate(BaseModel): name: Optional[str] = None email: Optional[str] = None age: Optional[int] = None @app.patch("/users/{user_id}") async def update_user(user_id: int, body: UserUpdate): update_data = body.model_dump(exclude_unset=True) # update_data only contains fields the client sent # {"name": "Alice"} instead of {"name": "Alice", "email": None, "age": None} return {"updated_fields": list(update_data.keys())} Without exclude_unset=True, every optional field gets included with None — and you'd accidentally overwrite real data with nulls. HTTPException with Headers Need to include custom headers in your error responses? from fastapi import HTTPException @app.get("/protected") async def protected_route(): raise HTTPException( status_code=401, detail="Token expired", headers={"X-Error-Code": "TOKEN_EXPIRED", "X-Retry-After": "3600"} ) Cleaner than manually building JSONResponse for every error. yield for Cleanup Perfect for database connections, file handles, or temporary resources: from fastapi import Depends async def get_db(): db = await connect_database() try: yield db # ↑ everything before yield = setup finally: await db.close() # ↓ runs after response is sent @app.get("/users") async def list_users(db=Depends(get_db)): users = await db.fetch("SELECT * FROM users") return users The finally block executes even if an exception occurs. This pattern keeps your routes clean and leak-free. Instead of try/except in every route, register a global handler: from fastapi import FastAPI, Request from fastapi.responses import JSONResponse class AppError(Exception): def __init__(self, code: str, message: str, status: int = 400): self.code = code self.message = message self.status = status app = FastAPI() @app.exception_handler(AppError) async def app_error_handler(request: Request, exc: AppError): return JSONResponse( status_code=exc.status, content={"error": {"code": exc.code, "message": exc.message}} ) @app.get("/users/{user_id}") async def get_user(user_id: int): user = await fetch_user(user_id) if not user: raise AppError("USER_NOT_FOUND", f"User {user_id} does not exist", 404) return user Consistent error format across your entire API with zero boilerplate per route. Don't trust client-side validation: from fastapi import UploadFile, File, HTTPException ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"} MAX_SIZE = 5 * 1024 * 1024 # 5MB @app.post("/upload") async def upload_file(file: UploadFile = File(...)): # Check file type if file.content_type not in ALLOWED_TYPES: raise HTTPException(400, f"File type '{file.content_type}' not allowed") # Read and check size content = await file.read() if len(content) > MAX_SIZE: raise HTTPException(400, f"File too large: {len(content)} bytes (max {MAX_SIZE})") # Process file... return {"filename": file.filename, "size": len(content)} BackgroundTasks for Non-Critical Work Send emails, write logs, or process images without blocking the response: from fastapi import BackgroundTasks async def send_welcome_email(user_email: str): # Simulate slow operation await asyncio.sleep(3) print(f"Welcome email sent to {user_email}") @app.post("/register") async def register(email: str, background_tasks: BackgroundTasks): user = await create_user(email) background_tasks.add_task(send_welcome_email, user.email) return {"message": "User created", "email_sent": "in_background"} The user gets an instant response. The email sends in the background. Always. Always add this: @app.get("/health") async def health_check(): return { "status": "healthy", "timestamp": datetime.now(timezone.utc).isoformat(), "version": "1.0.0" } Your monitoring tools, load balancers, and future self will thank you. import logging import sys # Configure structured logging at startup logging.basicConfig( level=logging.INFO, format='{"time":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}', handlers=[logging.StreamHandler(sys.stdout)] ) @app.middleware("http") async def log_requests(request: Request, call_next): logging.info(f"{request.method} {request.url.path}") response = await call_next(request) logging.info(f"→ {response.status_code}") return response JSON-formatted logs work great with ELK, CloudWatch, or any log aggregator. These tips come from real production issues I ran into. FastAPI is already developer-friendly, but knowing these patterns makes the experience even smoother. Which FastAPI tip do you use most? Did I miss something you rely on? Drop a comment below. 👇 Follow for more backend engineering content — next up: async database patterns that actually scale.
