FastAPI
Generic skill for writing FastAPI code following best practices, keeping consistency and avoiding common pitfalls (blocking calls in async routes, duplicated validation logic, untyped responses).
Quick Reference
- Project structure: split routers, schemas, and dependencies into separate modules; see Project Structure.
- Schemas: use Pydantic models for request/response validation, never raw dicts; see Pydantic Schemas.
- Routing: use
APIRouterper resource withprefix/tags; see Routers. - Dependency Injection: use
Depends()for shared logic (DB sessions, auth, pagination); see Dependency Injection and the dependencies reference for sub-dependencies,yielddependencies, andSecurity(). - Async: use
async defonly when the body is actually non-blocking; run blocking I/O in a thread pool; see Async. - Error handling: raise
HTTPExceptionfor expected errors; use exception handlers for cross-cutting cases; see Error Handling. - Testing: use
TestClient/httpx.AsyncClientwithpytest; see Testing and the testing reference for dependency overrides and async test clients.
Project Structure
myproject/
├── main.py
├── myproject/
│ ├── core/
│ │ ├── config.py
│ │ └── security.py
│ ├── api/
│ │ ├── deps.py
│ │ └── v1/
│ │ ├── routers/
│ │ │ └── clientes.py
│ │ └── __init__.py
│ ├── schemas/
│ │ └── cliente.py
│ ├── models/
│ │ └── cliente.py
│ └── services/
│ └── cliente.py
└── tests/
└── test_clientes.py
Keep path operation functions thin — validation lives in Pydantic schemas, business logic in a service layer, and persistence in models/repositories.
Pydantic Schemas
Separate input and output schemas instead of reusing one model for everything:
from pydantic import BaseModel, EmailStr
class ClienteCreate(BaseModel):
nome: str
email: EmailStr
class ClienteRead(BaseModel):
id: int
nome: str
email: EmailStr
class Config:
from_attributes = True
Use response_model on every path operation so FastAPI filters and documents the output shape:
@router.post("/clientes", response_model=ClienteRead, status_code=201)
def criar_cliente(payload: ClienteCreate) -> Cliente:
...
Routers
Group related path operations with APIRouter, and mount them in main.py:
# api/v1/routers/clientes.py
router = APIRouter(prefix="/clientes", tags=["clientes"])
@router.get("/{cliente_id}", response_model=ClienteRead)
def obter_cliente(cliente_id: int, service: ClienteService = Depends(get_cliente_service)):
return service.buscar_por_id(cliente_id)
# main.py
app.include_router(clientes.router, prefix="/api/v1")
Dependency Injection
Use Depends() for anything shared across path operations — DB sessions, current user, pagination params:
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
@router.get("/clientes")
def listar_clientes(db: Session = Depends(get_db)):
return db.query(Cliente).all()
See the dependencies reference for sub-dependencies, yield-based dependencies with cleanup, caching with Depends(..., use_cache=True), and Security() for auth scopes.
Async
Only declare a path operation async def if its body doesn't block the event loop. A blocking call (sync DB driver, requests, CPU-bound work) inside an async def route stalls every other request:
# Bad: sync DB call blocks the event loop
@router.get("/clientes")
async def listar_clientes(db: Session = Depends(get_db)):
return db.query(Cliente).all()
# Good: plain def lets FastAPI run it in a thread pool
@router.get("/clientes")
def listar_clientes(db: Session = Depends(get_db)):
return db.query(Cliente).all()
Use async def with an async driver (asyncpg, httpx.AsyncClient, async SQLAlchemy session) when every I/O call in the path is actually awaited.
Error Handling
Raise HTTPException for expected, per-request errors:
@router.get("/clientes/{cliente_id}", response_model=ClienteRead)
def obter_cliente(cliente_id: int, db: Session = Depends(get_db)):
cliente = db.get(Cliente, cliente_id)
if cliente is None:
raise HTTPException(status_code=404, detail="cliente não encontrado")
return cliente
Use an exception handler for errors that recur across many routes instead of repeating try/except in each one:
@app.exception_handler(ClienteNaoEncontrado)
def handle_cliente_nao_encontrado(request: Request, exc: ClienteNaoEncontrado):
return JSONResponse(status_code=404, content={"detail": str(exc)})
Testing
Use TestClient (sync) or httpx.AsyncClient (async) with pytest, overriding dependencies instead of hitting a real database or external service:
from fastapi.testclient import TestClient
client = TestClient(app)
def test_criar_cliente():
response = client.post("/api/v1/clientes", json={"nome": "João", "email": "joao@example.com"})
assert response.status_code == 201
assert response.json()["nome"] == "João"
See the testing reference for overriding dependencies with app.dependency_overrides, testing async routes with httpx.AsyncClient, and fixtures for a per-test database.
Tooling
- Package manager:
uvorpip-toolsfor reproducible dependency pinning. - ASGI server:
uvicornfor development,gunicorn -k uvicorn.workers.UvicornWorker(oruvicornwith multiple workers) in production. - Validation/serialization: Pydantic v2 — avoid mixing v1-style validators (
@validator) with v2 (@field_validator). - Lint/format: Ruff/Black.
- Docs: rely on the auto-generated OpenAPI schema (
/docs,/redoc) — keep schemas and status codes accurate rather than writing parallel API docs by hand.