Avoid These 10 Common FastAPI Pitfalls for Beginners
This guide lists ten typical mistakes that new FastAPI developers make—such as using synchronous I/O in async endpoints, ignoring Pydantic models, mishandling database sessions, creating per‑request clients, misconfiguring CORS, monolithic code files, returning raw ORM objects, weak authentication, lacking timeouts/retries for external calls, and skipping testing—and provides concrete solutions with code examples to keep applications fast, reliable, and maintainable.
FastAPI is praised for its modern, high‑performance design, async support, and automatic API documentation, but beginners often fall into traps that hurt performance and stability. The following ten pitfalls and their remedies help you build robust FastAPI services.
1. Using synchronous I/O in an async environment
Problem: Calling requests.get() or time.sleep() inside an async def endpoint blocks the event loop.
Impact: The blocked call pauses the entire event loop, drastically reducing concurrency.
Solution: Use a true async library or offload the sync work to a thread pool.
from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool
import httpx, time
app = FastAPI()
@app.get("/good")
async def good_endpoint():
async with httpx.AsyncClient(timeout=5) as client:
response = await client.get("https://example.com")
return {"length": len(response.text)}
@app.get("/okish")
async def okish_endpoint():
# If you must call sync code, use a thread pool
return {"done": await run_in_threadpool(time.sleep, 1)}2. Ignoring Pydantic models and response_model
Problem: Returning raw dictionaries or accepting arbitrary JSON skips data validation and type safety.
Impact: You lose automatic validation, accurate API docs, and type safety, leading to runtime errors.
Solution: Define request and response models explicitly.
from pydantic import BaseModel, Field
class UserIn(BaseModel):
email: str
name: str = Field(min_length=1)
class UserOut(BaseModel):
id: int
email: str
name: str
@app.post("/users", response_model=UserOut, status_code=201)
async def create_user(payload: UserIn):
user = {"id": 1, **payload.model_dump()}
return userUsing Pydantic models guards your data logic at the boundaries while keeping core business code clean.
3. Improper database session management
Problem: Database connections pile up; long‑lived transactions cause "too many connections" errors.
Impact: Each request needs a short‑lived session and must ensure resources are cleaned up.
Solution: Use a dependency that yields the session lifecycle.
from fastapi import Depends
from sqlalchemy.orm import Session
def get_db_session():
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
@app.get("/orders/{order_id}")
def get_order(order_id: int, db: Session = Depends(get_db_session)):
return db.get(Order, order_id)This pattern guarantees proper commit/rollback/close even when errors occur.
4. Creating a client/connection pool per request
Problem: Instantiating httpx.AsyncClient() or a DB engine inside an endpoint.
Impact: Extra handshake overhead, socket waste, and memory churn.
Solution: Manage shared resources via the application lifespan.
from contextlib import asynccontextmanager
import httpx
@asynccontextmanager
async def app_lifespan(app: FastAPI):
# Create resources at startup
app.state.http_client = httpx.AsyncClient(timeout=5)
yield
# Clean up at shutdown
await app.state.http_client.aclose()
app = FastAPI(lifespan=app_lifespan)
@app.get("/health")
async def health_check():
response = await app.state.http_client.get("https://example.com/health")
return {"status": "healthy" if response.status_code == 200 else "unhealthy"}5. Misconfiguring CORS
Problem: Setting allow_origins=["*"] together with allow_credentials=True .
Impact: Browsers reject this unsafe combination, resulting in CORS errors.
Solution: Explicitly specify allowed origins.
from fastapi.middleware.cors import CORSMiddleware
allowed_origins = ["https://app.company.com", "https://staging.company.com"]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)CORS should be treated as a security boundary , not an after‑thought.
6. Keeping all code in a single file
Problem: Handlers, models, and utilities live together, making changes unpredictable.
Impact: High coupling, difficult testing and extension.
Solution: Organize code with routers and packages.
app/
├─ main.py # app factory, lifespan, middleware
├─ api/
│ ├─ __init__.py
│ ├─ users.py # user‑related routes
│ └─ orders.py # order‑related routes
├─ models/ # Pydantic models
├─ db/ # DB engine, session, models
└─ dependencies/ # dependency utilitiesIn users.py:
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/{user_id}")
def get_user(user_id: int):
# business logic
return {"user_id": user_id}In main.py:
from app.api import users, orders
app.include_router(users.router)
app.include_router(orders.router)7. Returning raw ORM objects
Problem: Directly returning SQLAlchemy model instances leads to serialization quirks.
Impact: ORM objects are not plain JSON; they may expose sensitive fields or cause recursion.
Solution: Serialize via Pydantic models and from_attributes=True .
from pydantic import BaseModel, ConfigDict
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
name: str
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int, db: Session = Depends(get_db_session)):
user = db.get(User, user_id)
return userExplicitly declare the fields you want to expose—no more, no less.
8. Weak authentication and homemade security
Problem: Implementing custom token logic without scope checks leaves all routes public.
Impact: Security debt accumulates quickly.
Solution: Centralize authentication with a dependency.
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = verify_token(token) # your token verification logic
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user
@app.get("/users/me")
async def read_current_user(current_user: User = Depends(get_current_user)):
return {"id": current_user.id, "email": current_user.email}Use dependency injection at the router or router‑level to enforce authentication.
9. Missing timeout, retry, and rate‑limit for external calls
Problem: Downstream latency cascades, causing thread pile‑up and latency spikes.
Solution: Combine connection pools, timeouts, and exponential back‑off.
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.2, max=2))
async def fetch_data(client: httpx.AsyncClient, url: str):
response = await client.get(url, timeout=httpx.Timeout(2.0, connect=1.0))
response.raise_for_status()
return response.json()
@app.get("/external-data")
async def get_external_data(http_client: httpx.AsyncClient = Depends(get_http_client)):
data = await fetch_data(http_client, "https://api.external.com/data")
return {"data": data}A bounded retry policy with strict timeouts prevents a single downstream service from dragging down the whole application.
10. Ignoring testing and error handling
Problem: Manual cURL testing and hoping everything works.
Impact: Refactoring becomes hard; error messages are unfriendly.
Solution: Use TestClient , dependency overrides, and custom exception handlers.
from fastapi.testclient import TestClient
def override_get_current_user():
return FakeUser(id=1, email="[email protected]")
app.dependency_overrides[get_current_user] = override_get_current_user
client = TestClient(app)
def test_read_current_user():
response = client.get("/users/me")
assert response.status_code == 200
assert response.json()["email"] == "[email protected]" from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
return JSONResponse(status_code=400, content={"error": str(exc)})
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)Tests give a safety net for refactoring, while exception handlers keep noisy stack traces from leaking to clients.
Quick Checklist
Use true async libraries; offload unavoidable sync work to a thread pool.
Validate with Pydantic models and response_model.
Manage database sessions with yield dependencies.
Create shared client/connection pools in the application lifespan.
Lock down CORS policies.
Organize code with routers and packages.
Serialize DTOs with from_attributes=True .
Enforce authentication via dependencies.
Add timeout/retry mechanisms for external calls.
Write tests and exception handlers .
FastAPI offers great flexibility; applying these ten practices early prevents costly refactors and keeps latency low, helping you build robust, high‑performance APIs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Data STUDIO
Click to receive the "Python Study Handbook"; reply "benefit" in the chat to get it. Data STUDIO focuses on original data science articles, centered on Python, covering machine learning, data analysis, visualization, MySQL and other practical knowledge and project case studies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
