📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Backend com Python e FastAPI. Recomendado revisar antes de começar.
v1.0`
Neste projeto, construiremos uma API REST central e desacoplada para um sistema de “Lista de Tarefas” (To-Do List), utilizando a stack moderna de Python 3.11+, FastAPI, SQLAlchemy 2.0 e Pydantic v2.
A API funciona como a única fonte de verdade para qualquer frontend (Angular, React, Vue, HTMX, Mobile ou Desktop):
flowchart TD
subgraph "📱 Clientes (Frontends)"
Web["💻 Frontend Web<br>(Angular / React / Vue / HTMX)"]
Desktop["🖥️ Frontend Desktop<br>(Custom UI)"]
Mobile["📱 Frontend Mobile<br>(Android / Flutter / Ionic)"]
end
subgraph "⚙️ Serviços (Backend Python)"
API["🔌 Backend API REST<br>(FastAPI + Uvicorn)"]
DB[("🗄️ Banco de Dados<br>SQLite (Dev) / PostgreSQL (Prod)")]
end
%% Conexões de Dados
Web -->|"HTTP / JSON (CORS)"| API
Desktop -->|HTTP / JSON| API
Mobile -->|HTTP / JSON| API
API --- DB
%% Estilização
style API fill:#dfd,stroke:#333,stroke-width:2px
style DB fill:#def,stroke:#333,stroke-width:2px
style Web fill:#ffe,stroke:#333,stroke-width:2px
—`
| Camada / Papel | Stack Java (listadetarefas_01) |
Stack Python (listadetarefas_python_01) |
|---|---|---|
| Linguagem & Runtime | Java 17+ (JDK) | Python 3.11+ |
| Framework Web | Spring Boot 3.x (@RestController) |
FastAPI (APIRouter, auto docs Swagger) |
| Acesso a Dados | Spring Data JPA / Hibernate | SQLAlchemy 2.0 (Mapped, mapped_column) |
| DTOs & Validação | Lombok / Bean Validation | Pydantic v2 (BaseModel, Field) |
| Injeção de Dependências | @Autowired |
Depends() |
| Banco de Dados (Dev) | H2 em memória | SQLite (sqlite:///./tarefas.db) |
| Banco de Dados (Prod) | PostgreSQL | PostgreSQL (Neon) via psycopg |
| Testes | Spring Boot Test / JUnit | pytest + httpx (TestClient) |
| Documentação API | SpringDoc / Swagger | Swagger UI nativo em /docs |
—`
listadetarefas-api)Objetivo: Criar o serviço central RESTful em camadas que gerencia o ciclo de vida das tarefas (CRUD completo).
mkdir listadetarefas-api
cd listadetarefas-api
git init
git branch -M main
# Criar ambiente virtual Python 3.11+
python -m venv .venv
# Ativar o ambiente virtual:
# Windows (PowerShell):
.venv\Scripts\Activate.ps1
# Linux/macOS:
source .venv/bin/activate
requirements.txt:fastapi>=0.115.0
uvicorn[standard]>=0.30.0
sqlalchemy>=2.0.35
pydantic>=2.9.0
pydantic-settings>=2.5.0
psycopg[binary]>=3.2.0
pytest>=8.3.0
httpx>=0.27.0
Instale as dependências:
pip install -r requirements.txt
listadetarefas-api/
├── app/
│ ├── __init__.py
│ ├── config.py # Configurações e CORS
│ ├── database.py # Engine SQLAlchemy e SessionLocal
│ ├── models/
│ │ ├── __init__.py
│ │ └── tarefa.py # Entidade SQLAlchemy (tb_tarefas)
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── tarefa.py # DTOs Pydantic (Create, Update, Response)
│ ├── repositories/
│ │ ├── __init__.py
│ │ └── tarefa_repository.py # Operações no banco (find, save, delete)
│ ├── services/
│ │ ├── __init__.py
│ │ └── tarefa_service.py # Regras de negócio e exceções
│ ├── routers/
│ │ ├── __init__.py
│ │ └── tarefas.py # Endpoints HTTP REST
│ └── main.py # App FastAPI, CORS e Lifespan
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixtures de teste em memória
│ └── test_tarefas_api.py # Testes de integração pytest
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── README.md
└── requirements.txt
Arquivo: app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
APP_NAME: str = "API Lista de Tarefas"
DATABASE_URL: str = "sqlite:///./tarefas.db"
ENVIRONMENT: str = "dev"
CORS_ORIGINS: list[str] = ["*"]
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore"
)
settings = Settings()
Arquivo: app/database.py
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker, Session
from app.config import settings
db_url = settings.DATABASE_URL
if db_url.startswith("postgres://"):
db_url = db_url.replace("postgres://", "postgresql+psycopg://", 1)
elif db_url.startswith("postgresql://") and not db_url.startswith("postgresql+"):
db_url = db_url.replace("postgresql://", "postgresql+psycopg://", 1)
connect_args = {"check_same_thread": False} if db_url.startswith("sqlite") else {}
engine = create_engine(db_url, connect_args=connect_args)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
erDiagram
TB_TAREFAS {
INT id PK "Auto-incremento"
VARCHAR descricao "Descrição da tarefa"
BOOLEAN concluida "Status de conclusão"
}
Arquivo: app/models/tarefa.py
from sqlalchemy import String, Boolean
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class Tarefa(Base):
__tablename__ = "tb_tarefas"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
descricao: Mapped[str] = mapped_column(String(255), nullable=False)
concluida: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
Arquivo: app/schemas/tarefa.py
from pydantic import BaseModel, ConfigDict, Field
class TarefaBase(BaseModel):
descricao: str = Field(..., min_length=1, max_length=255, description="Descrição da tarefa")
concluida: bool = Field(default=False, description="Status de conclusão")
class TarefaCreate(TarefaBase):
pass
class TarefaUpdate(BaseModel):
descricao: str | None = Field(default=None, min_length=1, max_length=255)
concluida: bool | None = Field(default=None)
class TarefaResponse(TarefaBase):
id: int
model_config = ConfigDict(from_attributes=True)
classDiagram
direction LR
TarefasRouter ..> TarefaService : Usa
TarefaService ..> TarefaRepository : Usa
TarefaRepository ..> Tarefa : Gerencia
class TarefasRouter {
+listar_tarefas() List~TarefaResponse~
+buscar_tarefa(id: int) TarefaResponse
+criar_tarefa(tarefa: TarefaCreate) TarefaResponse
+atualizar_tarefa(id: int, tarefa: TarefaUpdate) TarefaResponse
+deletar_tarefa(id: int) void
}
class TarefaService {
+listar_todas() List~Tarefa~
+buscar_por_id(id: int) Tarefa
+criar(tarefa_in: TarefaCreate) Tarefa
+atualizar(id: int, tarefa_in: TarefaUpdate) Tarefa
+deletar(id: int) void
}
class TarefaRepository {
+find_all() List~Tarefa~
+find_by_id(id: int) Tarefa
+create(tarefa_in: TarefaCreate) Tarefa
+update(db_tarefa: Tarefa, tarefa_in: TarefaUpdate) Tarefa
+delete(db_tarefa: Tarefa) void
}
class Tarefa {
+int id
+str descricao
+bool concluida
}
Arquivo: app/repositories/tarefa_repository.py
from sqlalchemy.orm import Session
from app.models.tarefa import Tarefa
from app.schemas.tarefa import TarefaCreate, TarefaUpdate
class TarefaRepository:
def __init__(self, db: Session):
self.db = db
def find_all(self) -> list[Tarefa]:
return self.db.query(Tarefa).order_by(Tarefa.id.asc()).all()
def find_by_id(self, id: int) -> Tarefa | None:
return self.db.query(Tarefa).filter(Tarefa.id == id).first()
def create(self, tarefa_in: TarefaCreate) -> Tarefa:
db_tarefa = Tarefa(
descricao=tarefa_in.descricao,
concluida=tarefa_in.concluida,
)
self.db.add(db_tarefa)
self.db.commit()
self.db.refresh(db_tarefa)
return db_tarefa
def update(self, db_tarefa: Tarefa, tarefa_in: TarefaUpdate) -> Tarefa:
if tarefa_in.descricao is not None:
db_tarefa.descricao = tarefa_in.descricao
if tarefa_in.concluida is not None:
db_tarefa.concluida = tarefa_in.concluida
self.db.commit()
self.db.refresh(db_tarefa)
return db_tarefa
def delete(self, db_tarefa: Tarefa) -> None:
self.db.delete(db_tarefa)
self.db.commit()
Arquivo: app/services/tarefa_service.py
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.repositories.tarefa_repository import TarefaRepository
from app.schemas.tarefa import TarefaCreate, TarefaUpdate
from app.models.tarefa import Tarefa
class TarefaService:
def __init__(self, db: Session):
self.repository = TarefaRepository(db)
def listar_todas(self) -> list[Tarefa]:
return self.repository.find_all()
def buscar_por_id(self, id: int) -> Tarefa:
tarefa = self.repository.find_by_id(id)
if not tarefa:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tarefa não encontrada com o id: {id}",
)
return tarefa
def criar(self, tarefa_in: TarefaCreate) -> Tarefa:
return self.repository.create(tarefa_in)
def atualizar(self, id: int, tarefa_in: TarefaUpdate) -> Tarefa:
db_tarefa = self.buscar_por_id(id)
return self.repository.update(db_tarefa, tarefa_in)
def deletar(self, id: int) -> None:
db_tarefa = self.buscar_por_id(id)
self.repository.delete(db_tarefa)
Arquivo: app/routers/tarefas.py
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.schemas.tarefa import TarefaCreate, TarefaUpdate, TarefaResponse
from app.services.tarefa_service import TarefaService
router = APIRouter(prefix="/api/tarefas", tags=["Tarefas"])
def get_tarefa_service(db: Session = Depends(get_db)) -> TarefaService:
return TarefaService(db)
@router.get("", response_model=list[TarefaResponse], summary="Listar todas as tarefas")
def listar_tarefas(service: TarefaService = Depends(get_tarefa_service)):
return service.listar_todas()
@router.get("/{id}", response_model=TarefaResponse, summary="Buscar tarefa por ID")
def buscar_tarefa(id: int, service: TarefaService = Depends(get_tarefa_service)):
return service.buscar_por_id(id)
@router.post("", response_model=TarefaResponse, status_code=status.HTTP_201_CREATED, summary="Criar nova tarefa")
def criar_tarefa(tarefa_in: TarefaCreate, service: TarefaService = Depends(get_tarefa_service)):
return service.criar(tarefa_in)
@router.put("/{id}", response_model=TarefaResponse, summary="Atualizar tarefa existente")
def atualizar_tarefa(id: int, tarefa_in: TarefaUpdate, service: TarefaService = Depends(get_tarefa_service)):
return service.atualizar(id, tarefa_in)
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT, summary="Deletar tarefa por ID")
def deletar_tarefa(id: int, service: TarefaService = Depends(get_tarefa_service)):
service.deletar(id)
Arquivo: app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.database import Base, engine
from app.routers import tarefas_router
@asynccontextmanager
async def lifespan(app: FastAPI):
# Inicializa as tabelas do banco
Base.metadata.create_all(bind=engine)
yield
app = FastAPI(
title=settings.APP_NAME,
description="API RESTful de Lista de Tarefas com FastAPI e SQLAlchemy",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(tarefas_router)
pytestArquivo: tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.main import app
from app.database import Base, get_db
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def db_session():
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db_session):
def override_get_db():
try:
yield db_session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
Arquivo: tests/test_tarefas_api.py
def test_listar_tarefas_vazio(client):
response = client.get("/api/tarefas")
assert response.status_code == 200
assert response.json() == []
def test_criar_tarefa(client):
payload = {"descricao": "Estudar FastAPI", "concluida": False}
response = client.post("/api/tarefas", json=payload)
assert response.status_code == 201
data = response.json()
assert data["id"] == 1
assert data["descricao"] == "Estudar FastAPI"
assert data["concluida"] is False
def test_buscar_tarefa_por_id(client):
create_resp = client.post("/api/tarefas", json={"descricao": "Comprar pão", "concluida": False})
tarefa_id = create_resp.json()["id"]
response = client.get(f"/api/tarefas/{tarefa_id}")
assert response.status_code == 200
assert response.json()["descricao"] == "Comprar pão"
response_404 = client.get("/api/tarefas/999")
assert response_404.status_code == 404
def test_atualizar_tarefa(client):
create_resp = client.post("/api/tarefas", json={"descricao": "Lavar o carro", "concluida": False})
tarefa_id = create_resp.json()["id"]
update_resp = client.put(f"/api/tarefas/{tarefa_id}", json={"descricao": "Lavar o carro e aspirar", "concluida": True})
assert update_resp.status_code == 200
assert update_resp.json()["descricao"] == "Lavar o carro e aspirar"
assert update_resp.json()["concluida"] is True
def test_deletar_tarefa(client):
create_resp = client.post("/api/tarefas", json={"descricao": "Tarefa temporária", "concluida": False})
tarefa_id = create_resp.json()["id"]
delete_resp = client.delete(f"/api/tarefas/{tarefa_id}")
assert delete_resp.status_code == 204
get_resp = client.get(f"/api/tarefas/{tarefa_id}")
assert get_resp.status_code == 404
Execute os testes:
pytest -v
Inicie o servidor de desenvolvimento:
uvicorn app.main:app --reload --port 8000
O FastAPI gera automaticamente a documentação OpenAPI 3.1 e disponibiliza interfaces interativas para testar a API diretamente no navegador:
Acessar o Swagger:
Com o servidor rodando (uvicorn app.main:app --reload), abra http://127.0.0.1:8000/docs no navegador.
POST /api/tarefas):
POST /api/tarefas.{
"descricao": "Estudar FastAPI e Swagger",
"concluida": false
}
201 Created e o objeto com o id gerado (ex: {"id": 1, ...}).GET /api/tarefas):
GET /api/tarefas.Status 200 OK).GET /api/tarefas/{id}):
GET /api/tarefas/{id} → “Try it out”.id: 1 e clique em “Execute”.999) e veja o retorno 404 Not Found.PUT /api/tarefas/{id}):
PUT /api/tarefas/{id} → “Try it out”.id: 1 e no corpo:
{
"descricao": "Estudar FastAPI e Swagger (Concluído)",
"concluida": true
}
200 OK.DELETE /api/tarefas/{id}):
DELETE /api/tarefas/{id} → “Try it out”.id: 1 e clique em “Execute”.204 No Content.Arquivo: Dockerfile
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8000
EXPOSE 8000
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
Você concluiu a construção da API RESTful de Lista de Tarefas em Python 3.11+, utilizando FastAPI, SQLAlchemy 2.0, arquitetura em camadas e cobertura completa de testes automatizados com pytest.
Esta API está pronta para ser consumida por qualquer cliente Frontend (Angular, React, Vue, Mobile ou HTMX).
—`
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd python_web_tarefas_01_fastapi
pip install -r requirements.txt
uvicorn app:app --reload
# ou python main.py
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/python_web_tarefas_01_fastapi