Content to action
Qubicweb keeps the discovery and trust-education layer lightweight. When you need governed account, commerce, service, or trust actions, continue in the canonical app without losing the article’s source context.
Content to action
Qubicweb keeps the discovery and trust-education layer lightweight. When you need governed account, commerce, service, or trust actions, continue in the canonical app without losing the article’s source context.
Brief points
Key points will appear here once TrustOps condenses this read. Use the source link below if you need the full article immediately.
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.
Spot something off?