skills/backend/fastapi/SKILL.md
FastAPI best practices e convenções baseadas em produção real. Aplicar em todos os projetos FastAPI.
npx skillsauth add lucasbiason/cursor-multiagent-system fastapi-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Convenções e melhores práticas para projetos FastAPI baseadas em experiência de produção.
Última Atualização: 2026-01-20
Fonte: Baseado em experiência de produção de startups
Aplicar esta skill quando:
fastapi-project/
├── alembic/
├── src/
│ ├── {domain}/ # e.g., auth/, posts/, payments/
│ │ ├── router.py # API endpoints
│ │ ├── schemas.py # Pydantic models
│ │ ├── models.py # Database models
│ │ ├── service.py # Business logic
│ │ ├── dependencies.py # Route dependencies
│ │ ├── config.py # Environment variables (domain-specific)
│ │ ├── constants.py # Constants and error codes
│ │ ├── exceptions.py # Domain-specific exceptions
│ │ └── utils.py # Helper functions
│ ├── config.py # Global configuration
│ ├── models.py # Global models
│ ├── exceptions.py # Global exceptions
│ ├── pagination.py # Global modules
│ ├── database.py # Database connection
│ └── main.py # FastAPI app initialization
├── tests/
│ ├── {domain}/ # Tests organized by domain
│ └── conftest.py
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
├── .env
├── .env.example
├── .gitignore
├── alembic.ini
└── logging.ini
IMPORTANTE: Quando um módulo tem mais de uma classe, cada classe deve estar em um arquivo separado:
# ❌ ERRADO: Múltiplas classes no mesmo arquivo
src/auth/models.py:
- User
- Role
- Permission
# ✅ CORRETO: Uma classe por arquivo
src/auth/models/
├── __init__.py # Exporta todas as classes
├── user.py # class User
├── role.py # class Role
└── permission.py # class Permission
Padrão obrigatório:
__init__.py importa e exporta todas as classesfrom src.auth.models import User, Role, Permissionasync def routes: Use APENAS para operações não-bloqueantes (I/O com await)def routes: Use para operações bloqueantes (CPU-bound, cálculos pesados)await em função def# ✅ CORRETO: I/O não-bloqueante
@router.get("/posts")
async def get_posts():
posts = await database.fetch_all("SELECT * FROM posts")
return posts
# ✅ CORRETO: CPU-bound (bloqueante)
@router.post("/calculate")
def calculate(data: CalculationRequest):
result = heavy_computation(data) # Sem await
return result
# ❌ ERRADO: await em função def
@router.get("/posts")
def get_posts():
posts = await database.fetch_all("SELECT * FROM posts") # ERRO!
return posts
# ✅ CORRETO: Wrapper para função bloqueante
from fastapi.concurrency import run_in_threadpool
@router.post("/process")
async def process_data(data: ProcessRequest):
my_data = await service.get_my_data()
# Executar função bloqueante em thread pool
result = await run_in_threadpool(sync_client.make_request, data=my_data)
return result
from pydantic import BaseModel, ConfigDict
class BaseSchema(BaseModel):
model_config = ConfigDict(
from_attributes=True, # Permite ORM models
str_strip_whitespace=True,
validate_assignment=True,
)
from pydantic_settings import BaseSettings
class DatabaseSettings(BaseSettings):
host: str
port: int = 5432
user: str
password: str
class Config:
env_prefix = "DB_"
class APISettings(BaseSettings):
secret_key: str
algorithm: str = "HS256"
class Config:
env_prefix = "API_"
from pydantic import BaseModel, EmailStr, Field, AnyUrl
class UserCreate(BaseSchema):
email: EmailStr
password: str = Field(min_length=8, max_length=100)
age: int = Field(ge=18, le=120)
website: AnyUrl | None = None
class PostResponse(BaseSchema):
id: UUID4
title: str
content: str
@model_serializer
def ser_model(self) -> dict[str, Any]:
"""Return a dict which contains only serializable fields."""
return {
"id": str(self.id),
"title": self.title,
"content": self.content,
}
from fastapi import Depends, HTTPException
async def verify_token(token: str = Header(...)) -> dict:
if not is_valid(token):
raise HTTPException(401, "Invalid token")
return decode_token(token)
@router.get("/protected")
async def protected_route(user: dict = Depends(verify_token)):
return {"user": user}
async def get_current_user(token: str = Depends(verify_token)) -> dict:
user = await get_user_by_token(token)
if not user:
raise InvalidCredentials()
return user
async def verify_owner(
post_id: UUID4,
current_user: dict = Depends(get_current_user)
) -> dict:
post = await get_post(post_id)
if post["owner_id"] != current_user["id"]:
raise UserNotOwner()
return post
@router.delete("/posts/{post_id}")
async def delete_post(post: dict = Depends(verify_owner)):
await delete_post_by_id(post["id"])
return {"deleted": True}
Preferir operações no banco de dados:
# ✅ CORRETO: Agregação no banco
from sqlalchemy import desc, func, select, text
from sqlalchemy.sql.functions import coalesce
async def get_posts(creator_id: UUID4, *, limit: int = 10, offset: int = 0) -> list[dict[str, Any]]:
select_query = (
select(
(
posts.c.id,
posts.c.slug,
posts.c.title,
func.json_build_object(
text("'id', profiles.id"),
text("'first_name', profiles.first_name"),
text("'last_name', profiles.last_name"),
text("'username', profiles.username"),
).label("creator"),
)
)
.select_from(posts.join(profiles, posts.c.owner_id == profiles.c.id))
.where(posts.c.owner_id == creator_id)
.limit(limit)
.offset(offset)
.order_by(desc(coalesce(posts.c.updated_at, posts.c.published_at, posts.c.created_at)))
)
return await database.fetch_all(select_query)
Quando usar SQL puro:
Referências obrigatórias (templates/snippets do cursor-multiagent-system):
core/templates/database/fastapi-repository-snippet.py - Base repository com métodos _query_one, _query_list, _query_scalar para SQL puro segurocore/templates/database/django-sql-snippets.py - Funções genéricas para SQL puro (Django) com proteção contra SQL injectionProteção contra SQL Injection:
text() do SQLAlchemy com parâmetrosfrom sqlalchemy import text
from typing import Dict, Any, Optional
# ✅ CORRETO: SQL puro com parâmetros seguros
async def complex_query(user_id: UUID, filters: Dict[str, Any]) -> List[Dict]:
sql = text("""
SELECT
p.id,
p.title,
COUNT(c.id) as comment_count,
AVG(r.rating) as avg_rating
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
LEFT JOIN ratings r ON r.post_id = p.id
WHERE p.owner_id = :user_id
AND p.status = :status
AND p.created_at >= :start_date
GROUP BY p.id, p.title
HAVING COUNT(c.id) > :min_comments
ORDER BY avg_rating DESC
LIMIT :limit
""")
result = await database.fetch_all(
sql,
{
"user_id": str(user_id),
"status": filters.get("status", "published"),
"start_date": filters.get("start_date"),
"min_comments": filters.get("min_comments", 0),
"limit": filters.get("limit", 10)
}
)
return [dict(row) for row in result]
Quando usar cache:
Referência obrigatória (template/snippet do cursor-multiagent-system):
core/templates/cache/redis-cache-snippet.py - Sistema de cache Redis genérico (Django e standalone)Padrão de uso:
from core.templates.cache.redis_cache_snippet import CacheSystem
import os
# Configurar cache (exemplo com variáveis de ambiente)
cache = CacheSystem(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", 6379)),
password=os.getenv("REDIS_PASSWORD"),
db=0
)
async def get_user_profile(user_id: UUID) -> Dict:
cache_key = f"user_profile:{user_id}"
# Tentar cache primeiro
cached = cache.read(cache_key)
if cached:
return cached
# Se não estiver em cache, buscar do banco
profile = await database.fetch_one(
"SELECT * FROM profiles WHERE id = :id",
{"id": str(user_id)}
)
if profile:
# Salvar no cache (expira em 1 hora)
cache.save(cache_key, dict(profile), expiration=3600)
return dict(profile)
return None
2022-08-24_post_content_idx.pyfile_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s
TODO E QUALQUER AGENTE ESTÁ PROIBIDO DE MEXER EM MIGRATIONS JÁ APLICADAS:
Razão: Alterar migrations anteriores não tem efeito no banco de dados já migrado. Isso vale para Django e FastAPI (Alembic).
Processo correto:
from httpx import AsyncClient
from src.main import app
@pytest.mark.asyncio
async def test_create_post():
async with AsyncClient(app=app, base_url="http://test") as client:
resp = await client.post("/posts", json={"title": "Test"})
assert resp.status_code == 201
testing
Execução e análise de testes automatizados
development
Gera resumos didáticos extensos e estruturados de aulas/cursos para cards do Notion. Use ao resumir aulas, apostilas, transcrições ou materiais de estudo para incluir no corpo do card (não apenas no campo Descrição), com flashcards, exemplos de código, diagramas Mermaid, mapa conceitual e perguntas de reforço.
development
Padroniza documentação existente no formato canônico Spec-Driven. Remove duplicação e melhora rastreabilidade.
development
Processo universal e repetível para criar especificações a partir de qualquer input (texto, docs, código). Usado em Plan mode.