DEV Community

Suifeng023
Suifeng023

Posted on

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.


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())}
Enter fullscreen mode Exit fullscreen mode

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"}
    )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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"
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)