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.
1. Use 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.
2. Return 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.
3. Dependency Injection with 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.
4. Custom Exception Handlers
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.
5. Validate File Uploads Server-Side
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)}
6. Use 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.
7. Add a Health Check Endpoint
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.
Quick Bonus: Structured Logging
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.
Wrapping Up
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.
Top comments (0)