Projeto final: API REST completa com testes e documentação
1. Planejamento e Estrutura do Projeto
Uma API REST bem construída começa com planejamento sólido. Neste projeto final, desenvolveremos uma API de gerenciamento de tarefas (To-Do List) com operações CRUD completas, autenticação JWT e documentação automática.
Requisitos funcionais:
- Cadastro e autenticação de usuários
- Criação, listagem, atualização e exclusão de tarefas
- Controle por roles (admin e usuário comum)
- Paginação e filtros
Escolha do framework: Optamos pelo FastAPI por sua performance, validação nativa com Pydantic e geração automática de documentação OpenAPI.
Estrutura de diretórios:
todo_api/
├── app/
│ ├── main.py
│ ├── config.py
│ ├── models/
│ ├── schemas/
│ ├── routes/
│ ├── services/
│ ├── dependencies.py
│ └── database.py
├── tests/
├── migrations/
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
2. Modelagem de Dados e Configuração do Banco
Utilizamos SQLAlchemy como ORM e Pydantic para schemas de validação.
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "sqlite:///./todo.db"
SECRET_KEY: str = "sua-chave-secreta-aqui"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
class Config:
env_file = ".env"
settings = Settings()
# app/models.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
role = Column(String, default="user")
created_at = Column(DateTime, default=datetime.utcnow)
tasks = relationship("Task", back_populates="owner")
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String)
completed = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="tasks")
3. Implementação dos Endpoints da API REST
Criamos schemas Pydantic para validação de entrada/saída e rotas CRUD completas.
# app/schemas.py
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class TaskBase(BaseModel):
title: str
description: Optional[str] = None
completed: bool = False
class TaskCreate(TaskBase):
pass
class TaskResponse(TaskBase):
id: int
created_at: datetime
owner_id: int
class Config:
from_attributes = True
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserResponse(BaseModel):
id: int
username: str
email: str
role: str
is_active: bool
class Config:
from_attributes = True
# app/routes/tasks.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from app import schemas, models, dependencies
router = APIRouter(prefix="/tasks", tags=["tasks"])
@router.get("/", response_model=List[schemas.TaskResponse])
def list_tasks(
skip: int = 0,
limit: int = 100,
db: Session = Depends(dependencies.get_db),
current_user: models.User = Depends(dependencies.get_current_user)
):
tasks = db.query(models.Task).filter(
models.Task.owner_id == current_user.id
).offset(skip).limit(limit).all()
return tasks
@router.post("/", response_model=schemas.TaskResponse, status_code=status.HTTP_201_CREATED)
def create_task(
task: schemas.TaskCreate,
db: Session = Depends(dependencies.get_db),
current_user: models.User = Depends(dependencies.get_current_user)
):
db_task = models.Task(**task.model_dump(), owner_id=current_user.id)
db.add(db_task)
db.commit()
db.refresh(db_task)
return db_task
@router.put("/{task_id}", response_model=schemas.TaskResponse)
def update_task(
task_id: int,
task: schemas.TaskCreate,
db: Session = Depends(dependencies.get_db),
current_user: models.User = Depends(dependencies.get_current_user)
):
db_task = db.query(models.Task).filter(
models.Task.id == task_id,
models.Task.owner_id == current_user.id
).first()
if not db_task:
raise HTTPException(status_code=404, detail="Task not found")
for key, value in task.model_dump().items():
setattr(db_task, key, value)
db.commit()
db.refresh(db_task)
return db_task
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_task(
task_id: int,
db: Session = Depends(dependencies.get_db),
current_user: models.User = Depends(dependencies.get_current_user)
):
db_task = db.query(models.Task).filter(
models.Task.id == task_id,
models.Task.owner_id == current_user.id
).first()
if not db_task:
raise HTTPException(status_code=404, detail="Task not found")
db.delete(db_task)
db.commit()
4. Autenticação e Autorização
Implementamos autenticação JWT com controle de acesso por roles.
# app/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from typing import Optional
from app import models, schemas
from app.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> models.User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(models.User).filter(models.User.username == username).first()
if user is None:
raise credentials_exception
return user
def require_admin(current_user: models.User = Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Admin privileges required")
return current_user
5. Testes Automatizados
Testes com pytest cobrindo cenários de sucesso, erro e borda.
# tests/test_tasks.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
from app.dependencies import get_password_hash
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
@pytest.fixture
def client():
Base.metadata.create_all(bind=engine)
yield TestClient(app)
Base.metadata.drop_all(bind=engine)
def test_create_task(client):
# Primeiro cria um usuário e obtém token
client.post("/auth/register", json={
"username": "testuser",
"email": "test@example.com",
"password": "testpass123"
})
response = client.post("/auth/login", data={
"username": "testuser",
"password": "testpass123"
})
token = response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
response = client.post("/tasks/", json={
"title": "Minha tarefa",
"description": "Descrição da tarefa"
}, headers=headers)
assert response.status_code == 201
data = response.json()
assert data["title"] == "Minha tarefa"
assert data["completed"] == False
def test_list_tasks_empty(client):
client.post("/auth/register", json={
"username": "testuser2",
"email": "test2@example.com",
"password": "testpass123"
})
response = client.post("/auth/login", data={
"username": "testuser2",
"password": "testpass123"
})
token = response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
response = client.get("/tasks/", headers=headers)
assert response.status_code == 200
assert response.json() == []
def test_delete_nonexistent_task(client):
client.post("/auth/register", json={
"username": "testuser3",
"email": "test3@example.com",
"password": "testpass123"
})
response = client.post("/auth/login", data={
"username": "testuser3",
"password": "testpass123"
})
token = response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
response = client.delete("/tasks/999", headers=headers)
assert response.status_code == 404
6. Documentação da API
O FastAPI gera automaticamente documentação OpenAPI/Swagger. Customizamos com tags e exemplos.
# app/main.py
from fastapi import FastAPI
from app.routes import auth, tasks, admin
app = FastAPI(
title="Todo API",
description="API REST completa para gerenciamento de tarefas",
version="1.0.0",
contact={
"name": "Seu Nome",
"email": "seu@email.com"
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
)
app.include_router(auth.router, prefix="/auth", tags=["autenticação"])
app.include_router(tasks.router, prefix="/tasks", tags=["tarefas"])
app.include_router(admin.router, prefix="/admin", tags=["administração"])
Acesse http://localhost:8000/docs para visualizar a documentação interativa.
7. Empacotamento e Deploy
Conteinerização com Docker para deploy consistente.
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/todo_db
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
volumes:
- .:/app
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=todo_db
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
8. Validação Final e Boas Práticas
Checklist de qualidade:
- [ ] Testes passando com cobertura > 80%
- [ ] Documentação Swagger acessível
- [ ] Linters configurados (flake8, black, mypy)
- [ ] Variáveis sensíveis em variáveis de ambiente
- [ ] Docker build funcional
- [ ] Tratamento de erros consistente
Comandos úteis:
# Executar testes
pytest --cov=app tests/
# Verificar estilo de código
black --check app/
flake8 app/
mypy app/
# Build Docker
docker-compose build
docker-compose up -d
Referências
- FastAPI Official Documentation — Documentação oficial do FastAPI com tutoriais completos, guias de segurança e deploy
- SQLAlchemy ORM Guide — Guia rápido de início com SQLAlchemy ORM para modelagem de dados
- Pydantic V2 Documentation — Documentação oficial do Pydantic para validação de dados e schemas
- PyTest Documentation — Guia completo de testes com pytest, incluindo fixtures e mocking
- Docker Compose for Python Apps — Tutorial oficial de Docker Compose para aplicações Python
- JWT Authentication with FastAPI — Guia oficial de implementação de autenticação JWT no FastAPI
- Python Security Best Practices — OWASP Python Security Cheat Sheet com práticas recomendadas