📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Backend com Python e FastAPI e na Matriz de Aplicações Full Stack. Recomendado revisar antes de começar.
📁 Código deste projeto: subpasta
api-fastapi/. Você pode construir do zero seguindo a Seção 4 (recomendado: cada arquivo aparece completo, idêntico ao do repositório) ou abrir o código pronto e usar o tutorial como guia de leitura. Todos os comandos rodam de dentro da pastaapi-fastapi.✅ Testado em: Windows 11 com Python 3.11, 3.13 e 3.14, usando as versões fixadas no
requirements.txt.
Bem-vindo à versão em Python da fundação da Trilha Prática de Aplicações Full Stack! Este projeto é a contraparte homóloga oficial do projeto Parte 01 em Java / Spring Boot.
Nesta etapa, você aprenderá a construir um Backend RESTful de alta performance, adotando a stack moderna de Python 3.11+, FastAPI, SQLAlchemy 2.0 e Pydantic v2, organizado no padrão arquitetural em camadas (Clean Layered Architecture), com validação declarativa de dados, suíte de testes automatizados com pytest e homologação 100% visual no navegador via Swagger UI (/docs).
Antes de escrever qualquer linha de código em Python, um engenheiro de software profissional realiza o levantamento de requisitos. Construir software sem definir com clareza o que deve ser entregue e quais critérios comprovam o sucesso acarreta retrabalho, inconsistências de modelo e falhas de integração.
Neste projeto didático, aplicamos as melhores práticas da Engenharia de Software para que você aprenda a rastrear cada necessidade de negócio até os modelos, rotas e testes correspondentes.
Nosso cliente necessita de um serviço centralizado, leve e seguro para gerenciar sua rotina diária de afazeres. No modelo ágil, formalizamos essas necessidades no formato de Histórias de Usuário:
Os Requisitos Funcionais definem as operações e comportamentos observáveis que o sistema deve executar:
| ID | Nome do Requisito | Descrição Detalhada | Regra de Negócio Associada |
|---|---|---|---|
| RF-01 | Cadastrar Tarefa | O sistema deve criar um novo registro de tarefa a partir de uma descrição textual enviada via JSON. | A descrição é obrigatória: espaços nas pontas são removidos e o que sobra precisa ter de 1 a 255 caracteres. O identificador (id) é gerado automaticamente. Se concluida não for enviado, a tarefa nasce pendente (false). |
| RF-02 | Listar Todas as Tarefas | O sistema deve retornar a coleção completa de tarefas cadastradas, ordenadas por ID ascendente. | Se a base estiver vazia, deve retornar um array JSON vazio ([]) com status HTTP 200 OK. |
| RF-03 | Consultar Tarefa por ID | O sistema deve localizar e retornar os dados de uma tarefa a partir do seu identificador numérico. | Se o ID não existir na base de dados, a operação deve ser rejeitada com erro HTTP 404 Not Found. |
| RF-04 | Atualizar Tarefa | O sistema deve permitir alterar a descrição e/ou o status de conclusão de uma tarefa existente. | Campos omitidos no corpo permanecem como estão. Se o ID informado não existir na base, rejeitar com 404 Not Found. Se os dados forem inválidos, rejeitar com 422 Unprocessable Entity. |
| RF-05 | Alternar / Definir Status | O sistema deve fornecer duas operações curtas para o checkbox das interfaces: inverter o status (false ⇄ true, sem corpo) e definir o status informado ({"concluida": true}). |
Definir é idempotente (repetir o pedido dá o mesmo resultado); inverter não é. Se o ID não existir, retornar 404 Not Found. |
| RF-06 | Excluir Tarefa | O sistema deve remover permanentemente uma tarefa a partir do seu identificador numérico. | Se o ID existir, remover o registro e responder com status HTTP 204 No Content sem corpo. Se não existir, responder com 404 Not Found. |
Os Requisitos Não-Funcionais definem as restrições de qualidade, tecnologia, arquitetura e usabilidade que regem o sistema:
| ID | Categoria | Descrição do Critério | Justificativa Pedagógica & Técnica |
|---|---|---|---|
| RNF-01 | Arquitetura RESTful | A API deve operar sobre HTTP/1.1 e ASGI, expondo endpoints semânticos sob o prefixo /api/tarefas com payloads JSON. |
Garante desacoplamento total, permitindo que qualquer cliente (Web, Mobile, Desktop ou scripts) consuma a mesma API. |
| RNF-02 | Separação em Camadas | O código deve seguir estritamente o padrão em camadas: Router (exposição HTTP), Service (regras de negócio), Repository (persistência), Model (ORM relacional) e Schema (DTO Pydantic). | Evita acoplamento entre regras de negócio e infraestrutura de transporte ou banco, preparando o aluno para arquiteturas corporativas. |
| RNF-03 | Persistência Zero-Config | Em ambiente de desenvolvimento local, a API deve operar com SQLite local (tarefas.db), com suporte transparente a PostgreSQL em produção. |
Elimina barreiras de configuração inicial no Windows para alunos iniciantes, sem demandar Docker ou instalação de servidores locais de banco de dados. |
| RNF-04 | Autodocumentação OpenAPI & Swagger | A API deve gerar dinamicamente a especificação OpenAPI 3.1 e disponibilizar interfaces interativas em /docs (Swagger UI) e /redoc (ReDoc). |
Permite testar todos os verbos e inspecionar schemas e contratos diretamente pelo navegador, sem necessidade imediata de clientes externos. |
| RNF-05 | Validação Declarativa e Fail-Fast | A validação de payloads de entrada (inclusive descrição só com espaços) deve ocorrer na borda via Pydantic v2, retornando códigos de status semânticos (200 OK, 201 Created, 204 No Content, 404 Not Found, 422 Unprocessable Entity). |
Garante integridade imediata dos dados antes de qualquer execução de regras de negócio ou gravação no banco de dados. |
O diagrama abaixo sintetiza como os atores (aluno no navegador via Swagger UI, cliente frontend SPA ou extensão de requisições HTTP) interagem com o sistema:
flowchart LR
User(["👤 Aluno / Cliente HTTP"])
subgraph Sistema ["📦 API de Lista de Tarefas (FastAPI)"]
direction TB
UC1(["UC-01: Cadastrar Tarefa (RF-01)"])
UC2(["UC-02: Listar Tarefas (RF-02)"])
UC3(["UC-03: Consultar por ID (RF-03)"])
UC4(["UC-04: Atualizar Tarefa (RF-04)"])
UC5(["UC-05: Alternar ou Definir Conclusão (RF-05)"])
UC6(["UC-06: Excluir Tarefa (RF-06)"])
UC7(["UC-07: Inspecionar e Testar no Swagger UI (/docs)"])
UC8(["UC-08: Health Check (/ e /openapi.json)"])
end
User --> UC1
User --> UC2
User --> UC3
User --> UC4
User --> UC5
User --> UC6
User -.->|Navegador| UC7
User -.->|Monitoramento| UC8
style Sistema fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
style UC1 fill:#1e293b,stroke:#34d399,color:#f8fafc
style UC2 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style UC3 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style UC4 fill:#1e293b,stroke:#fbbf24,color:#f8fafc
style UC5 fill:#1e293b,stroke:#a78bfa,color:#f8fafc
style UC6 fill:#1e293b,stroke:#f87171,color:#f8fafc
style UC7 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style UC8 fill:#1e293b,stroke:#94a3b8,color:#f8fafc
A Matriz de Rastreabilidade assegura que cada requisito possua implementação comprovada em todas as camadas da aplicação:
| Requisito | Modelo ORM | Schema Pydantic | Repositório | Camada de Serviço | Rota da API (Router) | Teste Automatizado (pytest) |
|---|---|---|---|---|---|---|
| RF-01 | Tarefa (tb_tarefas) |
TarefaCreate |
TarefaRepository.create() |
TarefaService.criar() |
POST /api/tarefas (201) |
test_criar_tarefa |
| RF-02 | Tarefa (tb_tarefas) |
list[TarefaResponse] |
TarefaRepository.find_all() |
TarefaService.listar_todas() |
GET /api/tarefas (200) |
test_listar_tarefas_vazio |
| RF-03 | Tarefa (tb_tarefas) |
TarefaResponse |
TarefaRepository.find_by_id() |
TarefaService.buscar_por_id() |
GET /api/tarefas/{id} (200/404) |
test_buscar_tarefa_por_id |
| RF-04 | Tarefa (tb_tarefas) |
TarefaUpdate |
TarefaRepository.update() |
TarefaService.atualizar() |
PUT /api/tarefas/{id} (200/404) |
test_atualizar_tarefa, test_atualizacao_parcial_mantem_campos_omitidos |
| RF-05 | Tarefa (tb_tarefas) |
TarefaResponse |
TarefaRepository.toggle() |
TarefaService.alternar_status() |
PATCH /api/tarefas/{id}/toggle (200/404) |
test_alternar_status_tarefa |
| RF-05 | Tarefa (tb_tarefas) |
TarefaStatusUpdate |
TarefaRepository.update() |
TarefaService.definir_status() |
PATCH /api/tarefas/{id}/status (200/404) |
test_definir_status_tarefa |
| RF-06 | Tarefa (tb_tarefas) |
- | TarefaRepository.delete() |
TarefaService.deletar() |
DELETE /api/tarefas/{id} (204/404) |
test_deletar_tarefa |
| RF-03 a RF-06 | - | - | - | buscar_por_id() |
Todas as rotas com {id} (404) |
test_id_inexistente_retorna_404_em_todas_as_rotas |
| RNF-01 | - | - | - | - | @router + JSON + CORS |
test_cors_libera_frontend_em_outra_porta, test_health_check |
| RNF-02 | models/ |
schemas/ |
repositories/ |
services/ |
routers/ |
Inspeção da árvore e injeção Depends() |
| RNF-03 | Base SQLAlchemy |
- | SessionLocal |
- | get_db generator |
Testes com SQLite em memória (StaticPool) |
| RNF-04 | - | json_schema_extra |
- | - | docs_url="/docs" |
Acesso interativo a /docs e /openapi.json |
| RNF-05 | - | Field(min_length=1), str_strip_whitespace |
- | - | status_code semânticos |
test_validacao_pydantic_schema_erro_422, test_espacos_das_pontas_sao_removidos |
Toda a aplicação opera sob o servidor ASGI Uvicorn na porta padrão 8000, fornecendo a API REST centralizada e o ecossistema de documentação viva:
flowchart TD
subgraph UvicornService ["⚙️ Backend FastAPI & Uvicorn (Porta 8000)"]
direction TB
AppMain["🚀 main.py<br>(FastAPI App, Lifespan e CORS)"]
Router["🔌 routers/tarefas.py<br>(APIRouter /api/tarefas)"]
Schemas["🛡️ schemas/tarefa.py<br>(Pydantic v2: Create, Update, Status, Response)"]
Service["🧠 services/tarefa_service.py<br>(Regras de Negócio e Exceções)"]
Repo["🗄️ repositories/tarefa_repository.py<br>(Consultas e Operações de Banco)"]
DBEngine["💾 database.py & models/tarefa.py<br>(SQLAlchemy 2.0 Engine & Mapeamento)"]
SQLite[("📁 tarefas.db<br>(Banco Relacional SQLite)")]
AppMain --> Router
Router --> Schemas
Router --> Service
Service --> Repo
Repo --> DBEngine
DBEngine --> SQLite
end
subgraph Clientes ["👥 Como Você Irá Interagir"]
Swagger["🌐 Swagger UI (/docs)<br>Testes visuais interativos no navegador"]
ReDoc["📖 ReDoc (/redoc)<br>Documentação técnica para engenharia"]
External["📱 Frontends & Clientes Externos<br>Angular, Svelte, Vue, React, YARC, cURL"]
end
Swagger <-->|HTTP / JSON interativo| AppMain
ReDoc <-->|HTTP / Contrato OpenAPI 3.1| AppMain
External <-->|"HTTP / JSON (com CORS)"| AppMain
style UvicornService fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
style Clientes fill:#1e293b,stroke:#34d399,stroke-width:2px,color:#f8fafc
Antes de abrir o código, vamos esclarecer os quatro conceitos essenciais que fundamentam o ecossistema FastAPI.
Imagine um restaurante moderno:
422 Unprocessable Entity antes mesmo de entrar na cozinha!/docs).JSON (JavaScript Object Notation) é a linguagem franca da comunicação entre sistemas web. No FastAPI, você não precisa converter dicionários manualmente com json.dumps() ou json.loads(). O Pydantic v2 valida os dados de entrada na conversão de JSON para objetos Python e serializa automaticamente as respostas de volta para JSON segundo o modelo TarefaResponse.
Exemplo de payload JSON trafegado na rede (resposta de GET /api/tarefas/1):
{
"descricao": "Aprender FastAPI e Pydantic v2",
"concluida": false,
"id": 1
}
💡 O
idaparece por último porqueTarefaResponseherdadescricaoeconcluidadeTarefaBasee só depois declaraid. Em JSON a ordem das chaves não muda o significado.
| Operação | Método HTTP | Rota da API | Papel no Sistema | Analogia |
|---|---|---|---|---|
| Create (Criar) | POST |
/api/tarefas |
Cadastra nova tarefa | Escrever um novo lembrete |
| Read (Listar) | GET |
/api/tarefas |
Retorna a coleção completa | Consultar a folha de tarefas |
| Read (Buscar) | GET |
/api/tarefas/{id} |
Busca uma tarefa específica | Ler os detalhes de um item |
| Update (Editar) | PUT |
/api/tarefas/{id} |
Altera descrição e/ou status | Reescrever o texto do lembrete |
| Toggle (Alternar) | PATCH |
/api/tarefas/{id}/toggle |
Inverte o status (false ⇄ true) |
Marcar/desmarcar a caixinha (checkbox) |
| Definir Status | PATCH |
/api/tarefas/{id}/status |
Grava o status enviado no corpo | Escrever “feito” na caixinha, esteja como estiver |
| Delete (Excluir) | DELETE |
/api/tarefas/{id} |
Remove permanentemente o registro | Riscar e jogar fora o bilhete |
Abaixo vemos o pipeline percorrido desde a chegada do pacote HTTP via ASGI até a persistência no banco relacional:
Para quem vem do projeto Java de referência ou deseja dominar ambas as linguagens, este quadro mapeia a correspondência exata de cada componente:
| Camada / Responsabilidade | ☕ Java & Spring Boot (fullstack_tarefas_01_spring_api) |
🐍 Python & FastAPI (python_web_tarefas_01_fastapi) |
|---|---|---|
| Runtime & Linguagem | Java 17+ (JDK) com JVM | Python 3.11+ com interpretador CPython |
| Framework Web | Spring Boot (@RestController, @GetMapping, etc.) |
FastAPI (APIRouter, @router.get, decorators nativos) |
| Servidor de Execução | Tomcat embutido (Thread-per-request / Servlets) | Uvicorn (servidor ASGI; usa uvloop no Linux/macOS e o asyncio padrão no Windows) |
| Mapeamento ORM | Jakarta Persistence / Hibernate (@Entity, @Table) |
SQLAlchemy 2.0 (Base, Mapped, mapped_column) |
| Abstração de Repositório | Spring Data JPA (interface extends JpaRepository) |
Repository Pattern em classe com SessionLocal |
| DTOs e Validação | Lombok (@Data) + Jakarta Bean Validation |
Pydantic v2 (BaseModel, Field, ConfigDict) |
| Injeção de Dependências | @Autowired ou Injeção via Construtor |
Depends() nativo do FastAPI |
| Banco Local (Dev) | H2 Database em memória (jdbc:h2:mem:testdb) |
SQLite em arquivo local (sqlite:///./tarefas.db) |
| Testes Automatizados | JUnit 5 + Spring Boot Test + MockMvc | pytest + TestClient do FastAPI (sobre httpx2) com fixtures em memória |
| Documentação da API | SpringDoc OpenAPI / Swagger UI dependência externa | Swagger UI nativo em /docs e ReDoc em /redoc |
| Homologação Visual | Interface SSR com Thymeleaf ou extensão YARC | Swagger UI interativo nativo com botão Try it out |
Este tutorial foi estruturado especificamente para execução no Windows, com orientações completas para Visual Studio Code e JetBrains PyCharm.
Antes de começar, verifique a versão do Python instalada no seu terminal (PowerShell ou Prompt de Comando):
python --version
A saída deve indicar Python 3.11 ou superior (ex: Python 3.11.x, Python 3.13.x ou Python 3.14.x).
💡 Se
python --versionabrir a Microsoft Store ou disser que o comando não existe, use o inicializadorpyque vem com o instalador oficial:py --version(e, nos comandos abaixo,py -m venv .venvno lugar depython -m venv .venv). Depois que o ambiente virtual estiver ativo,pythonpassa a funcionar normalmente.
⚠️ Dica Fundamental para PowerShell no Windows: Ao tentar ativar ambientes virtuais pela primeira vez, o Windows pode exibir o erro
cannot be loaded because running scripts is disabled on this system. Para liberar a execução de scripts no seu usuário de forma segura, abra o PowerShell e execute:Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -Force
ℹ️ A pasta
api-fastapie o ambiente.venvsão criados nos Passos 0 e 1 logo abaixo. Se você está construindo do zero, faça os Passos 0 e 1 primeiro e volte aqui para apontar a IDE para o.venv.
Ctrl+K Ctrl+O).api-fastapi (a que você criou no Passo 0 ou, no código pronto, catalogo_projetos\python_web_tarefas_01_fastapi\api-fastapi).Ctrl+Shift+P, digite Python: Select Interpreter e escolha o interpretador do ambiente virtual .venv criado.Ctrl+\ (o VS Code ativará o (.venv)` automaticamente no rodapé do terminal).api-fastapi (a do Passo 0 ou a do código pronto) e abra como projeto..venv\Scripts\python.exe.api-fastapi, selecione Mark Directory as > Sources Root. Isso garante que as importações absolutas como from app.config import settings sejam reconhecidas com autocomplete completo sem alertas amarelos.Escolha onde guardar seus projetos (o exemplo usa C:\projetos) e crie a pasta api-fastapi com as subpastas das camadas. No PowerShell:
mkdir C:\projetos\api-fastapi
cd C:\projetos\api-fastapi
# Pastas das camadas e dos testes
mkdir app, app\models, app\schemas, app\repositories, app\services, app\routers, tests
Cada pasta com código Python precisa de um arquivo __init__.py para virar pacote (é o que permite escrever from app.config import settings). Crie os oito arquivos abaixo. Os de app/ e tests/ têm só um comentário; os das camadas reexportam a classe principal, e o main.py depende disso (from app.routers import tarefas_router):
app/__init__.py# Módulo de Inicialização da Aplicação
app/models/__init__.pyfrom app.models.tarefa import Tarefa
__all__ = ["Tarefa"]
app/schemas/__init__.pyfrom app.schemas.tarefa import TarefaCreate, TarefaUpdate, TarefaStatusUpdate, TarefaResponse
__all__ = ["TarefaCreate", "TarefaUpdate", "TarefaStatusUpdate", "TarefaResponse"]
app/repositories/__init__.pyfrom app.repositories.tarefa_repository import TarefaRepository
__all__ = ["TarefaRepository"]
app/services/__init__.pyfrom app.services.tarefa_service import TarefaService
__all__ = ["TarefaService"]
app/routers/__init__.pyfrom app.routers.tarefas import router as tarefas_router
__all__ = ["tarefas_router"]
tests/__init__.py# Pacote de Testes da API de Tarefas
⚠️ Os
__init__.pydas camadas importam arquivos que você só vai escrever nos Passos 3 a 7. Até lá, o Python acusariaModuleNotFoundErrorse você rodasse a aplicação; isso é esperado. O primeiro teste de execução é no Passo 8.
Crie também o .gitignore, para o Git não versionar o ambiente virtual, o banco local e o arquivo de segredos:
.gitignore.venv/
__pycache__/
*.pyc
*.db
.pytest_cache/
.coverage
htmlcov/
.env
.DS_Store
requirements.txt e requirements-dev.txt)As dependências ficam em dois arquivos. O requirements.txt tem o que a API precisa para rodar (é o que vai para a imagem Docker). As versões estão fixadas com ==: são as versões testadas neste tutorial, e fixá-las evita que uma versão nova de alguma biblioteca quebre o projeto no seu computador.
requirements.txtfastapi==0.143.0
starlette==1.7.0
uvicorn[standard]==0.54.0
sqlalchemy==2.1.4
pydantic==2.14.0
pydantic-settings==2.15.0
psycopg[binary]==3.3.6
💡 O
starlettenão é usado diretamente no código, mas é o motor do FastAPI (requisições, respostas, CORS,TestClient). O FastAPI aceita várias versões dele, por isso fixamos a versão aqui também.
O requirements-dev.txt inclui o arquivo anterior (-r requirements.txt) e acrescenta as ferramentas de teste: o pytest e o httpx2, cliente HTTP que o TestClient usa por baixo:
requirements-dev.txt-r requirements.txt
pytest==9.1.1
httpx2==2.13.1
O pytest.ini diz ao pytest onde estão os testes e coloca a pasta api-fastapi no caminho de importação, para que from app.main import app funcione nos testes:
pytest.ini[pytest]
pythonpath = .
testpaths = tests
Para criar o ambiente virtual e instalar as dependências no terminal do Windows (dentro da pasta api-fastapi):
# Criação do ambiente virtual na pasta da API
python -m venv .venv
# Ativação do ambiente virtual no Windows
.venv\Scripts\Activate.ps1
# Instalação das dependências (execução + testes)
pip install -r requirements-dev.txt
🎯 Checkpoint de Verificação 1: Ambiente e Dependências
- O que você acabou de implementar: Um ambiente virtual isolado com FastAPI, Uvicorn, SQLAlchemy 2.0 e Pydantic v2.
- Como validar agora: Execute
pip listno terminal com o.venvativo e confirmefastapi 0.143.0,pydantic 2.14.0,SQLAlchemy 2.1.4epytest 9.1.1na lista. O início do terminal deve mostrar(.venv).- Reflexão de Engenharia de Software (RNF-03): O uso de ambientes virtuais garante reprodutibilidade hermética: as dependências do projeto não interferem no Python global do sistema operacional.
config.py e database.py)A gestão de configurações adota o Pydantic Settings, lendo variáveis de ambiente de arquivos .env com tipagem estrita.
app/config.pyfrom 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()
app/database.pyfrom 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
# Compatibilidade automática para provedores que usam "postgres://" (Neon/Heroku)
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]:
"""Injeta a sessão do banco de dados na requisição e garante o fechamento ao final."""
db = SessionLocal()
try:
yield db
finally:
db.close()
.env.example# Configurações para Desenvolvimento Local (SQLite)
APP_NAME=API Lista de Tarefas
DATABASE_URL=sqlite:///./tarefas.db
ENVIRONMENT=dev
CORS_ORIGINS=["*"]
# Configurações para Produção (PostgreSQL Neon)
# DATABASE_URL=postgresql+psycopg://neondb_owner:SENHA@ep-xxx.neon.tech/neondb?sslmode=require
# ENVIRONMENT=prod
# CORS_ORIGINS=["https://meu-frontend.vercel.app"]
O .env.example é um modelo e não é lido pela aplicação. Sem nenhum .env, valem os valores padrão do config.py (SQLite local e CORS liberado), então ele é opcional para rodar localmente. Para mudar alguma configuração, copie o modelo para .env (copy .env.example .env) e edite a cópia; o .env fica fora do Git por causa do .gitignore.
🎯 Checkpoint de Verificação 2: Conexão e Sessão de Banco de Dados
- O que você acabou de implementar: A fábrica de conexões SQLAlchemy (
engine), o gerador de sessões (get_db) e a abstração de configurações com suporte a SQLite local e PostgreSQL.- Como validar agora: Confirme que os arquivos
config.pyedatabase.pyestão salvos dentro da pastaapp/.- Reflexão de Engenharia de Software (RNF-03): Com o gerador
get_db(), usamos o padrão Context Management: o FastAPI garante que a conexão do banco é aberta sob demanda e sempre fechada com segurança após o envio da resposta HTTP.
models/tarefa.py)No SQLAlchemy 2.0, as tabelas são mapeadas utilizando o sistema declarativo moderno baseado em type hints (Mapped e mapped_column).
erDiagram
TB_TAREFAS {
INT id PK "Chave Primária com Auto-Incremento"
VARCHAR(255) descricao "Texto descritivo da tarefa (NOT NULL)"
BOOLEAN concluida "Estado de conclusão (DEFAULT False)"
}
app/models/tarefa.pyfrom 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)
🎯 Checkpoint de Verificação 3: Mapeamento Objeto-Relacional (ORM)
- O que você acabou de implementar: A classe de modelo
Tarefamapeada para a tabela físicatb_tarefas.- Como validar agora: Certifique-se de que o arquivo
app/models/__init__.pyexiste para que o Python reconheça o pacote.- Reflexão de Engenharia de Software (RF-01, RNF-02): A entidade encapsula o estado persistido (
id,descricao,concluida). O SQLAlchemy traduzirá essa classe automaticamente para instruções SQLCREATE TABLEeINSERT INTOsem comandos manuais.
schemas/tarefa.py)Diferente do modelo ORM (que conversa com o banco), os Schemas do Pydantic definem o que é aceito e o que é retornado pela rede (DTO - Data Transfer Object).
classDiagram
direction TB
class TarefaBase {
+str descricao
+bool concluida
}
class TarefaCreate {
<<DTO Entrada POST>>
}
class TarefaUpdate {
<<DTO Entrada PUT>>
+str descricao (opcional)
+bool concluida (opcional)
}
class TarefaStatusUpdate {
<<DTO Entrada PATCH /status>>
+bool concluida
}
class TarefaResponse {
<<DTO Resposta JSON>>
+int id
}
TarefaBase <|-- TarefaCreate
TarefaBase <|-- TarefaResponse
app/schemas/tarefa.pyfrom pydantic import BaseModel, ConfigDict, Field
class TarefaBase(BaseModel):
# str_strip_whitespace remove os espaços das pontas antes de validar:
# assim " " vira "" e é recusado pelo min_length=1
model_config = ConfigDict(str_strip_whitespace=True)
descricao: str = Field(..., min_length=1, max_length=255, description="Descrição detalhada da tarefa")
concluida: bool = Field(default=False, description="Status de conclusão da tarefa (false = pendente, true = concluída)")
class TarefaCreate(TarefaBase):
model_config = ConfigDict(
json_schema_extra={
"example": {
"descricao": "Estudar FastAPI e Pydantic v2",
"concluida": False,
}
}
)
class TarefaUpdate(BaseModel):
descricao: str | None = Field(default=None, min_length=1, max_length=255, description="Nova descrição da tarefa")
concluida: bool | None = Field(default=None, description="Novo status de conclusão")
model_config = ConfigDict(
str_strip_whitespace=True,
json_schema_extra={
"example": {
"descricao": "Estudar FastAPI e Pydantic v2 (Concluído)",
"concluida": True,
}
},
)
class TarefaStatusUpdate(BaseModel):
concluida: bool = Field(..., description="Status desejado (true = concluída, false = pendente)")
model_config = ConfigDict(
json_schema_extra={
"example": {
"concluida": True,
}
}
)
class TarefaResponse(TarefaBase):
id: int = Field(..., description="Identificador único numérico gerado pelo banco de dados")
model_config = ConfigDict(
from_attributes=True,
json_schema_extra={
"example": {
"id": 1,
"descricao": "Estudar FastAPI e Pydantic v2",
"concluida": False,
}
},
)
🎯 Checkpoint de Verificação 4: Contratos de Dados e Validação Declarativa
- O que você acabou de implementar: Os esquemas de validação de entrada e serialização de saída com exemplos interativos para a documentação Swagger.
- Como validar agora: Observe a linha
from_attributes=True: ela permite que o Pydantic leia diretamente os atributos do modelo SQLAlchemy (db_tarefa.id,db_tarefa.descricao).- Reflexão de Engenharia de Software (RNF-05): Ao declarar
min_length=1, o Pydantic impede que requisições com descrições vazias ("") poluam o banco de dados, retornando status422 Unprocessable Entityde forma padronizada. Sem ostr_strip_whitespace=True, uma descrição" "(só espaços) passaria nessa regra; com ele, os espaços das pontas saem antes da validação e omin_lengthrecusa.- Por que
TarefaUpdatenão herda deTarefaBase? NoPUT, todos os campos são opcionais (None= “não mexer”). Por isso ele repete ostr_strip_whitespacena sua própria configuração.
repositories/tarefa_repository.py)O repositório é responsável exclusivamente por executar operações no banco de dados via SQLAlchemy.
app/repositories/tarefa_repository.pyfrom 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()
def toggle(self, db_tarefa: Tarefa) -> Tarefa:
db_tarefa.concluida = not db_tarefa.concluida
self.db.commit()
self.db.refresh(db_tarefa)
return db_tarefa
🎯 Checkpoint de Verificação 5: Abstração da Camada de Persistência
- O que você acabou de implementar: A classe
TarefaRepositorycom todos os métodos de persistência relacional (find_all,find_by_id,create,update,deleteetoggle).- Como validar agora: Verifique se as chamadas de transação (
self.db.commit()eself.db.refresh()) estão presentes nas operações que alteram o banco.- Reflexão de Engenharia de Software (RNF-02): Pelo padrão Repository Pattern, isolamos os comandos do ORM. Caso seja necessário trocar de banco ou otimizar consultas complexas, a camada de serviço permanece 100% inalterada.
services/tarefa_service.py)A camada de serviço concentra a lógica de negócio e o tratamento de exceções HTTP semânticas.
app/services/tarefa_service.pyfrom 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)
def alternar_status(self, id: int) -> Tarefa:
db_tarefa = self.buscar_por_id(id)
return self.repository.toggle(db_tarefa)
def definir_status(self, id: int, concluida: bool) -> Tarefa:
db_tarefa = self.buscar_por_id(id)
return self.repository.update(db_tarefa, TarefaUpdate(concluida=concluida))
🎯 Checkpoint de Verificação 6: Encapsulamento das Regras de Negócio
- O que você acabou de implementar: A classe
TarefaService, responsável por orquestrar o repositório e disparar o erro HTTP404 Not Foundcaso o registro não exista.- Como validar agora: Note que o método
buscar_por_idé reutilizado ematualizar,deletar,alternar_statusedefinir_status, garantindo que a verificação de existência seja sempre executada. Odefinir_statusnem precisa de método novo no repositório: reaproveita oupdatecom umTarefaUpdateque só temconcluida.- Reflexão de Engenharia de Software (RF-01 a RF-06): Centralizar as exceções de negócio na camada de serviço evita duplicação de validações nos controladores HTTP.
routers/tarefas.py)O roteador expõe os endpoints HTTP utilizando decorators semânticos e injeção de dependências nativa via Depends().
app/routers/tarefas.pyfrom fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.schemas.tarefa import TarefaCreate, TarefaUpdate, TarefaStatusUpdate, 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",
description="Retorna a coleção completa de tarefas cadastradas, ordenadas de forma crescente por ID. Se não houver tarefas, retorna uma lista vazia `[]` com status 200 OK."
)
def listar_tarefas(service: TarefaService = Depends(get_tarefa_service)):
return service.listar_todas()
@router.get(
"/{id}",
response_model=TarefaResponse,
summary="Buscar tarefa por ID",
description="Localiza e retorna uma tarefa específica pelo seu identificador único. Se não encontrada, retorna erro 404 Not Found."
)
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",
description="Cadastra uma nova tarefa na base de dados. O identificador numérico é gerado automaticamente; se `concluida` for omitido, a tarefa nasce pendente (`false`)."
)
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",
description="Atualiza a descrição e/ou o status de conclusão de uma tarefa existente pelo seu ID. Campos omitidos permanecem como estão. Se a tarefa não existir, retorna 404 Not Found."
)
def atualizar_tarefa(id: int, tarefa_in: TarefaUpdate, service: TarefaService = Depends(get_tarefa_service)):
return service.atualizar(id, tarefa_in)
@router.patch(
"/{id}/toggle",
response_model=TarefaResponse,
summary="Alternar status de conclusão (Toggle)",
description="Inverte o estado de conclusão da tarefa entre pendente (false) e concluída (true), sem corpo na requisição. Se a tarefa não existir, retorna 404 Not Found."
)
def alternar_status_tarefa(id: int, service: TarefaService = Depends(get_tarefa_service)):
return service.alternar_status(id)
@router.patch(
"/{id}/status",
response_model=TarefaResponse,
summary="Definir status de conclusão",
description="Define o status de conclusão informado no corpo (`{\"concluida\": true}`). É a rota usada pelos clientes React e Svelte da trilha. Se a tarefa não existir, retorna 404 Not Found."
)
def definir_status_tarefa(id: int, status_in: TarefaStatusUpdate, service: TarefaService = Depends(get_tarefa_service)):
return service.definir_status(id, status_in.concluida)
@router.delete(
"/{id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Deletar tarefa por ID",
description="Remove permanentemente uma tarefa do banco de dados pelo seu ID. Se bem-sucedido, retorna 204 No Content sem corpo. Se não encontrada, retorna 404 Not Found."
)
def deletar_tarefa(id: int, service: TarefaService = Depends(get_tarefa_service)):
service.deletar(id)
🎯 Checkpoint de Verificação 7: Exposição Semântica da API REST
- O que você acabou de implementar: Todas as rotas CRUD padronizadas (
GET,POST,PUT,PATCHeDELETE), com tipagem de saída declarada (response_model) e códigos de status HTTP apropriados.- Como validar agora: Observe o parâmetro
service: TarefaService = Depends(get_tarefa_service): ele resolve a injeção do serviço automaticamente a cada requisição.- Toggle ou Status? As duas rotas mudam
concluida, mas de jeitos diferentes.PATCH /togglenão tem corpo e inverte o valor atual: se o usuário clicar duas vezes rápido, a tarefa volta ao estado inicial.PATCH /statusrecebe o valor desejado e é idempotente: repetir o pedido não muda o resultado. É a rota que os clientes React e Svelte da trilha chamam.- Reflexão de Engenharia de Software (RNF-01, RNF-05): Ao documentar cada rota com
summaryedescription, o FastAPI enriquece automaticamente o Swagger UI com explicações claras para o desenvolvedor frontend.
main.py)O arquivo app/main.py reúne os middlewares, a configuração do OpenAPI e o ciclo de vida da aplicação.
app/main.pyfrom 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 import models # noqa: F401 (registra a tabela tb_tarefas no Base antes do create_all)
from app.routers import tarefas_router
@asynccontextmanager
async def lifespan(app: FastAPI):
# Criação automática das tabelas no banco de dados na inicialização
Base.metadata.create_all(bind=engine)
yield
tags_metadata = [
{
"name": "Tarefas",
"description": "Operações completas de CRUD (Create, Read, Update, Delete) e alternância do status de conclusão (Toggle e Status).",
},
{
"name": "Health",
"description": "Endpoint de monitoramento de integridade e links rápidos para documentação.",
},
]
app = FastAPI(
title=settings.APP_NAME,
description="""
# 🚀 API RESTful de Lista de Tarefas (To-Do List)
Backend profissional de referência para gerenciamento de tarefas desenvolvido com **Python 3.11+**, **FastAPI**, **SQLAlchemy 2.0** e **Pydantic v2**.
### 🌟 Destaques da Arquitetura:
* **Padrão em Camadas:** Separação estrita de responsabilidades (*Router* → *Service* → *Repository* → *Model* / *Schema*).
* **Autodocumentação OpenAPI 3.1:** Documentação interativa em tempo real com Swagger UI e ReDoc.
* **Validação Declarativa:** Schemas Pydantic v2 com validação estrita de tipos e sanitização de dados.
* **Persistência Relacional:** SQLite em desenvolvimento e suporte a PostgreSQL para produção.
* **CORS Habilitado:** Suporte para integração com frontends modernos (Angular, React, Vue, Svelte, Mobile).
""",
version="1.0.0",
openapi_tags=tags_metadata,
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
)
# Configuração de CORS para permitir requisições de frontends externos (Angular, React, Vue, Svelte)
# allow_credentials=False: a API não usa cookies, e o navegador recusa "*" junto com credenciais
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(tarefas_router)
@app.get("/", tags=["Health"])
def health_check():
return {
"status": "online",
"app": settings.APP_NAME,
"docs": "/docs",
"api": "/api/tarefas"
}
🎯 Checkpoint de Verificação 8: Inicialização do Servidor ASGI e Tabelas
- O que você acabou de implementar: A instância principal do FastAPI com suporte a CORS, manipulador
lifespane registro do roteador de tarefas. A linhafrom app import modelsgarante que a classeTarefajá foi carregada (e registrada noBase) quando ocreate_allroda; sem ela, o banco seria criado sem a tabelatb_tarefas.- Como validar agora: Inicie o servidor via terminal:
uvicorn app.main:app --reloade acessehttp://127.0.0.1:8000/. A resposta deve ser{"status":"online","app":"API Lista de Tarefas","docs":"/docs","api":"/api/tarefas"}, e o arquivotarefas.dbaparece na pastaapi-fastapi.- Reflexão de Engenharia de Software (RNF-01, RNF-04): O suporte a CORS (Cross-Origin Resource Sharing) é fundamental: sem ele, requisições feitas por aplicações Angular, React ou Svelte rodando em outras portas (ex: 4200, 5173) seriam bloqueadas pelo navegador por razões de segurança. Usamos
allow_credentials=Falseporque a API não usa cookies nem login, e o navegador recusa a combinação de origem*com credenciais. Em produção, troque o*pela URL real do frontend (veja o.env.example).
pytest (Garantia de Qualidade dos RFs)A suíte de testes automatizados utiliza um banco de dados SQLite em memória (sqlite:///:memory:) com StaticPool, garantindo que cada teste execute de forma rápida, hermética e sem poluir o banco de desenvolvimento.
tests/conftest.pyimport 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()
tests/test_tarefas_api.pydef 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):
# Cria tarefa
create_resp = client.post("/api/tarefas", json={"descricao": "Comprar pão", "concluida": False})
tarefa_id = create_resp.json()["id"]
# Busca por ID existente
response = client.get(f"/api/tarefas/{tarefa_id}")
assert response.status_code == 200
assert response.json()["descricao"] == "Comprar pão"
# Busca por ID inexistente
response_404 = client.get("/api/tarefas/999")
assert response_404.status_code == 404
def test_atualizar_tarefa(client):
# Cria tarefa
create_resp = client.post("/api/tarefas", json={"descricao": "Lavar o carro", "concluida": False})
tarefa_id = create_resp.json()["id"]
# Atualiza tarefa (marca como concluída)
update_resp = client.put(f"/api/tarefas/{tarefa_id}", json={"descricao": "Lavar o carro e aspirar", "concluida": True})
assert update_resp.status_code == 200
data = update_resp.json()
assert data["descricao"] == "Lavar o carro e aspirar"
assert data["concluida"] is True
def test_deletar_tarefa(client):
# Cria tarefa
create_resp = client.post("/api/tarefas", json={"descricao": "Tarefa temporária", "concluida": False})
tarefa_id = create_resp.json()["id"]
# Deleta tarefa
delete_resp = client.delete(f"/api/tarefas/{tarefa_id}")
assert delete_resp.status_code == 204
# Confirma que foi removida
get_resp = client.get(f"/api/tarefas/{tarefa_id}")
assert get_resp.status_code == 404
def test_alternar_status_tarefa(client):
# Cria tarefa pendente
create_resp = client.post("/api/tarefas", json={"descricao": "Comprar café", "concluida": False})
tarefa_id = create_resp.json()["id"]
assert create_resp.json()["concluida"] is False
# Primeiro toggle: false -> true
patch_resp = client.patch(f"/api/tarefas/{tarefa_id}/toggle")
assert patch_resp.status_code == 200
assert patch_resp.json()["concluida"] is True
# Segundo toggle: true -> false
patch_resp2 = client.patch(f"/api/tarefas/{tarefa_id}/toggle")
assert patch_resp2.status_code == 200
assert patch_resp2.json()["concluida"] is False
def test_definir_status_tarefa(client):
# Cria tarefa pendente
create_resp = client.post("/api/tarefas", json={"descricao": "Pagar a conta de luz"})
tarefa_id = create_resp.json()["id"]
# Define como concluída (rota usada pelos clientes React e Svelte)
resp = client.patch(f"/api/tarefas/{tarefa_id}/status", json={"concluida": True})
assert resp.status_code == 200
assert resp.json()["concluida"] is True
# Repetir o mesmo pedido não inverte de volta (diferente do toggle)
resp2 = client.patch(f"/api/tarefas/{tarefa_id}/status", json={"concluida": True})
assert resp2.json()["concluida"] is True
assert resp2.json()["descricao"] == "Pagar a conta de luz"
# Corpo sem o campo obrigatório
assert client.patch(f"/api/tarefas/{tarefa_id}/status", json={}).status_code == 422
def test_validacao_pydantic_schema_erro_422(client):
# Tenta criar tarefa com descricao vazia (violando min_length=1)
resp_vazio = client.post("/api/tarefas", json={"descricao": "", "concluida": False})
assert resp_vazio.status_code == 422
# Tenta criar tarefa sem o campo obrigatorio 'descricao'
resp_sem_campo = client.post("/api/tarefas", json={"concluida": False})
assert resp_sem_campo.status_code == 422
# Tenta enviar tipo invalido para 'concluida'
resp_tipo_invalido = client.post("/api/tarefas", json={"descricao": "Valida", "concluida": "nao_booleano"})
assert resp_tipo_invalido.status_code == 422
# Descrição só com espaços (os espaços são removidos e sobra "")
resp_espacos = client.post("/api/tarefas", json={"descricao": " ", "concluida": False})
assert resp_espacos.status_code == 422
def test_espacos_das_pontas_sao_removidos(client):
resp = client.post("/api/tarefas", json={"descricao": " Estudar SQL "})
assert resp.status_code == 201
assert resp.json()["descricao"] == "Estudar SQL"
def test_atualizacao_parcial_mantem_campos_omitidos(client):
create_resp = client.post("/api/tarefas", json={"descricao": "Ler um capítulo", "concluida": False})
tarefa_id = create_resp.json()["id"]
# Envia só o status: a descrição continua a mesma
resp = client.put(f"/api/tarefas/{tarefa_id}", json={"concluida": True})
assert resp.status_code == 200
assert resp.json()["descricao"] == "Ler um capítulo"
assert resp.json()["concluida"] is True
def test_id_inexistente_retorna_404_em_todas_as_rotas(client):
assert client.put("/api/tarefas/999", json={"descricao": "X"}).status_code == 404
assert client.patch("/api/tarefas/999/toggle").status_code == 404
assert client.patch("/api/tarefas/999/status", json={"concluida": True}).status_code == 404
assert client.delete("/api/tarefas/999").status_code == 404
resp = client.get("/api/tarefas/999")
assert resp.json() == {"detail": "Tarefa não encontrada com o id: 999"}
def test_cors_libera_frontend_em_outra_porta(client):
# Pré-requisição (preflight) que o navegador faz antes de um POST vindo do Vite (porta 5173)
resp = client.options(
"/api/tarefas",
headers={
"Origin": "http://localhost:5173",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "content-type",
},
)
assert resp.status_code == 200
assert resp.headers["access-control-allow-origin"] == "*"
def test_health_check(client):
resp = client.get("/")
assert resp.status_code == 200
assert resp.json()["status"] == "online"
Para rodar todos os testes no terminal com relatório detalhado (dentro da pasta api-fastapi, com o .venv ativo):
pytest -v
Resultado esperado: os 13 testes listados com PASSED e a linha final 13 passed. Os testes usam o banco em memória: o seu tarefas.db não é tocado.
/docs)Um dos maiores diferenciais do FastAPI sobre os frameworks tradicionais é a geração 100% automática e nativa da especificação OpenAPI 3.1.
Em vez de depender de ferramentas externas ou extensões de terceiros, o desenvolvedor pode testar, validar e demonstrar todas as funcionalidades do sistema diretamente pelo navegador!
Com a aplicação rodando via Uvicorn na porta 8000:
No Swagger UI, os métodos HTTP são categorizados por cores universais:
GET): Operações seguras de leitura e consulta que não alteram o estado do banco.POST): Criação de novos recursos na base de dados (status 201 Created).PUT): Substituição ou atualização completa de um recurso existente.PATCH): Modificação parcial de um recurso (como os nossos endpoints toggle e status).DELETE): Exclusão permanente de um recurso (status 204 No Content).Siga esta sequência prática no Swagger UI para homologar visualmente todos os Requisitos Funcionais:
GET /api/tarefas e clique nela para expandir.200[] (array vazio, confirmando que a base de dados iniciou limpa).POST /api/tarefas.{
"descricao": "Estudar fundamentos de FastAPI e Uvicorn",
"concluida": false
}
201{
"descricao": "Estudar fundamentos de FastAPI e Uvicorn",
"concluida": false,
"id": 1
}
POST /api/tarefas, altere o Request body para:
{
"descricao": "Dominar a documentação interativa Swagger UI",
"concluida": false
}
201 e retorno com "id": 2.GET /api/tarefas e clique em “Execute”.200 exibindo o array com as duas tarefas cadastradas:
[
{
"descricao": "Estudar fundamentos de FastAPI e Uvicorn",
"concluida": false,
"id": 1
},
{
"descricao": "Dominar a documentação interativa Swagger UI",
"concluida": false,
"id": 2
}
]
GET /api/tarefas/{id}.id, digite 1 e clique em “Execute”.
200 com os dados da Tarefa 1.999, e clique em “Execute”:
404 Not Found com a mensagem semântica {"detail": "Tarefa não encontrada com o id: 999"}.PATCH /api/tarefas/{id}/toggle.id: 1 e clique em “Execute”."concluida": true!"concluida": false. Cada chamada inverte o valor atual.PATCH /api/tarefas/{id}/status e clique em “Try it out”.id: 1 e use o corpo de exemplo {"concluida": true}."concluida": true. Diferente do toggle, repetir o pedido não desfaz a marcação.PUT /api/tarefas/{id}.id: 1 e cole no corpo:
{
"descricao": "Estudar fundamentos de FastAPI e Uvicorn (Revisado)",
"concluida": true
}
200 com a descrição atualizada e status refletido.{"concluida": false}: a descrição continua a mesma, porque campos omitidos não são alterados.POST /api/tarefas, experimente enviar uma descrição vazia:
{
"descricao": "",
"concluida": false
}
422 Unprocessable Entity com diagnóstico detalhado apontando exatamente que o campo descricao violou a restrição de tamanho mínimo (String should have at least 1 character).DELETE /api/tarefas/{id}.id: 2 e clique em “Execute”.204 No Content (sem corpo, comprovando exclusão bem-sucedida).GET /api/tarefas: apenas a Tarefa 1 permanece na lista.| Checkpoint | Operação / Ação de Teste | Critério de Aceite | Status |
|---|---|---|---|
| CP-01 | GET /api/tarefas (Carga Inicial) |
Status HTTP 200 OK, retornando array JSON vazio []. |
PASS ✅ |
| CP-02 | POST /api/tarefas (Nova Tarefa) |
Status HTTP 201 Created, corpo com id: 1 gerado pelo SQLite. |
PASS ✅ |
| CP-03 | GET /api/tarefas (Listagem com Dados) |
Status HTTP 200 OK, contendo a coleção completa de tarefas. |
PASS ✅ |
| CP-04 | GET /api/tarefas/1 (Busca Pontual) |
Status HTTP 200 OK para ID existente e 404 Not Found para inexistente. |
PASS ✅ |
| CP-05 | PATCH /api/tarefas/1/toggle (Toggle) |
Status HTTP 200 OK, invertendo o booleano de conclusão a cada chamada. |
PASS ✅ |
| CP-05b | PATCH /api/tarefas/1/status (Definir) |
Status HTTP 200 OK, com concluida igual ao valor enviado, mesmo repetindo o pedido. |
PASS ✅ |
| CP-06 | PUT /api/tarefas/1 (Edição) |
Status HTTP 200 OK, refletindo o novo texto da tarefa. |
PASS ✅ |
| CP-07 | POST /api/tarefas (Payload Inválido) |
Status HTTP 422 Unprocessable Entity com diagnóstico Pydantic. |
PASS ✅ |
| CP-08 | DELETE /api/tarefas/2 (Exclusão) |
Status HTTP 204 No Content sem corpo de resposta. |
PASS ✅ |
A estrutura de arquivos do projeto em api-fastapi/ organiza com precisão cada camada arquitetural:
api-fastapi/
├── app/
│ ├── __init__.py ← Marca a pasta app como pacote Python
│ ├── config.py ← Configurações Pydantic Settings, CORS e URLs de banco
│ ├── database.py ← Engine SQLAlchemy, SessionLocal e gerador get_db
│ ├── main.py ← Ponto de entrada FastAPI, metadados OpenAPI e CORS
│ ├── models/ ← Camada de Entidades e Mapeamento Objeto-Relacional (ORM)
│ │ ├── __init__.py
│ │ └── tarefa.py ← Modelo SQLAlchemy da tabela 'tb_tarefas'
│ ├── schemas/ ← Camada de DTOs e Validação Declarativa Pydantic v2
│ │ ├── __init__.py
│ │ └── tarefa.py ← Schemas TarefaCreate, TarefaUpdate, TarefaStatusUpdate e TarefaResponse
│ ├── repositories/ ← Camada de Acesso a Dados (Repository Pattern)
│ │ ├── __init__.py
│ │ └── tarefa_repository.py ← Operações SQL (find_all, find_by_id, create, toggle, delete)
│ ├── services/ ← Camada de Regras de Negócio e Exceções
│ │ ├── __init__.py
│ │ └── tarefa_service.py ← Regras de validação de existência e tratamento 404
│ └── routers/ ← Camada de Controladores HTTP REST
│ ├── __init__.py
│ └── tarefas.py ← Endpoints da API REST (/api/tarefas)
├── tests/ ← Suíte de Testes Automatizados com pytest
│ ├── __init__.py
│ ├── conftest.py ← Fixtures de banco SQLite em memória com StaticPool
│ └── test_tarefas_api.py ← Testes de integração dos Requisitos Funcionais
├── .dockerignore ← Arquivos ignorados no build Docker
├── .env.example ← Modelo de variáveis de ambiente (copie para .env)
├── .gitignore ← Arquivos ignorados pelo Git (.venv, __pycache__, *.db)
├── Dockerfile ← Imagem containerizada de produção com Python 3.11-slim
├── pytest.ini ← Configuração do pytest (pasta de testes e caminho de importação)
├── requirements.txt ← Dependências de execução, com versões fixadas
└── requirements-dev.txt ← requirements.txt + ferramentas de teste (pytest, httpx2)
🎯 Checklist de Validação da Estrutura:
- Todos os módulos possuem
__init__.pynos subdiretórios deapp/? ✅- O arquivo
tarefas.dbé gerado localmente na raiz deapi-fastapiao iniciar a API? ✅- O arquivo
requirements.txtlistafastapi,uvicorn,sqlalchemyepydanticcom versões fixadas (==)? ✅- Os testes automatizados em
tests/executam de forma isolada sem alterar o banco de desenvolvimento? ✅
Você pode executar o servidor de desenvolvimento de quatro formas simples, de acordo com o seu fluxo de trabalho preferido:
api-fastapi no VS Code..venv está selecionado (canto inferior direito ou via Ctrl+Shift+P > Python: Select Interpreter).Ctrl+\`) e inicie o servidor com auto-reload:
uvicorn app.main:app --reload --port 8000
Ctrl+C no terminal.app/main.py no PyCharm.api-fastapi está marcada como Sources Root (clique direito na pasta $\rightarrow$ Mark Directory as > Sources Root).uvicorn app.main:app --reload --port 8000
app/main.py. No PyCharm Community, escolha Python, troque Script path por Module name = uvicorn, preencha Parameters com app.main:app --reload --port 8000 e Working directory com a pasta api-fastapi. Clique no botão verde ▶️ (Run).api-fastapi (a que você criou no Passo 0; no código pronto, ela fica em catalogo_projetos\python_web_tarefas_01_fastapi\api-fastapi):
cd C:\projetos\api-fastapi
.\.venv\Scripts\Activate.ps1
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
Quando aparecer a mensagem Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit), o servidor estará pronto para receber requisições!
ℹ️ Esta opção é opcional. As Opções 1 a 3 rodam o projeto completo (API, Swagger e testes) só com o Python instalado: o banco SQLite já vem com o Python e não precisa de servidor. Use o Docker apenas se quiser praticar containerização ou publicar a API.
Se tiver o Docker Desktop instalado e aberto no Windows, crie os dois arquivos abaixo na pasta api-fastapi:
Dockerfile# Imagem base Python 3.11 slim
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Só as dependências de execução: pytest e httpx2 (requirements-dev.txt) ficam fora da imagem
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}"]
.dockerignore.venv
__pycache__
*.pyc
*.db
.pytest_cache
tests
.git
.env
Depois, no PowerShell:
cd C:\projetos\api-fastapi
# Build da imagem Docker
docker build -t listadetarefas-fastapi .
# Execução do container na porta 8000
docker run -p 8000:8000 listadetarefas-fastapi
ℹ️ O
tarefas.dbdo container fica dentro dele: ao remover o container, as tarefas somem. Para guardar os dados, use um volume ou um PostgreSQL (variávelDATABASE_URL).
| Sintoma / Erro | Causa Mais Provável | Solução Passo a Passo |
|---|---|---|
cannot be loaded because running scripts is disabled on this system |
Política de execução de scripts bloqueada no PowerShell do Windows. | Execute no PowerShell: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -Force e tente ativar o .venv novamente. |
ModuleNotFoundError: No module named 'app' |
O comando uvicorn foi executado a partir do diretório incorreto ou o PyCharm não reconheceu o Sources Root. |
Garanta que o terminal está exatamente dentro da pasta api-fastapi/ (onde a pasta app/ é visível). No PyCharm, clique com botão direito em api-fastapi $\rightarrow$ Mark Directory as > Sources Root. |
[Errno 10048] error while attempting to bind on address ('127.0.0.1', 8000) |
Outro processo (ou instância anterior do Uvicorn) já está ocupando a porta 8000. | No terminal, rode uvicorn app.main:app --reload --port 8001 e acesse via http://127.0.0.1:8001/docs. Alternativamente, feche outros terminais abertos. |
| O VS Code ou PyCharm sublinha imports como não encontrados | O interpretador global do Windows foi selecionado em vez do .venv. |
No VS Code: pressione Ctrl+Shift+P $\rightarrow$ Python: Select Interpreter $\rightarrow$ escolha .venv. No PyCharm: configure em Settings > Project > Python Interpreter. |
422 Unprocessable Entity no Swagger ou cliente |
O payload JSON enviado não atende às regras do Pydantic (ex: campo descricao ausente ou vazio). |
Inspecione a mensagem detalhada em detail retornada no corpo da resposta e envie um JSON com "descricao": "texto valido". |
sqlite3.OperationalError: database is locked |
Múltiplas instâncias simultâneas tentando gravar no arquivo tarefas.db. |
Feche processos concorrentes ou exclua o arquivo tarefas.db (o FastAPI recriará o banco automaticamente na próxima inicialização). |
| Erro de CORS ao chamar a API a partir de um frontend local | A origem do cliente não está listada ou o middleware CORS foi desabilitado. | Em app/config.py, confirme que CORS_ORIGINS: list[str] = ["*"] está ativo para permitir testes locais irrestritos. Se você criou um .env, confira a linha CORS_ORIGINS dele: o .env tem prioridade. |
pip install falha com Microsoft Visual C++ 14.0 or greater is required ou tenta compilar algum pacote |
A versão do Python não tem pacote pronto (wheel) para alguma dependência fixada. | Use Python 3.11, 3.12, 3.13 ou 3.14 (testados). Apague a pasta .venv, recrie com a versão certa (py -3.13 -m venv .venv) e instale de novo. |
pytest acusa ModuleNotFoundError: No module named 'app' |
O pytest foi chamado fora da pasta api-fastapi ou o pytest.ini não foi criado. |
Rode pytest -v de dentro de api-fastapi e confira se o pytest.ini existe com pythonpath = .. |
ImportError: cannot import name 'tarefas_router' from 'app.routers' |
O app/routers/__init__.py foi criado vazio. |
Copie o conteúdo do Passo 0: ele reexporta o router com o nome tarefas_router. O mesmo vale para os outros __init__.py das camadas. |
Agora você compreende a equivalência técnica entre as duas maiores linguagens do ecossistema corporativo moderno:
| Critério Pedagógico | ☕ Java + Spring Boot (fullstack_tarefas_01_spring_api) |
🐍 Python + FastAPI (python_web_tarefas_01_fastapi) |
|---|---|---|
| Estilo da Arquitetura | Padrão corporativo estrito orientado a interfaces e tipos compilados. | Padrão modular ágil orientado a type hints e concisão de código. |
| Curva de Aprendizado | Mais íngreme (exige JDK, anotações JPA, Lombok e Maven). | Mais suave (código conciso, tipagem intuitiva e inicialização rápida). |
| Validação de Dados | Anotações @Valid e @NotNull do Bean Validation. |
Schemas declarativos do Pydantic v2 com coerção automática de tipos. |
| Testes Visuais Iniciais | Interface renderizada no servidor (Thymeleaf) e extensão YARC. | Swagger UI nativo (/docs) com botão interativo Try it out. |
| Desempenho de E/S | Alto rendimento com pools de threads gerenciados. | Rotas async def aproveitam o loop de eventos ASGI. Neste projeto as rotas são def comuns (o SQLAlchemy usado é síncrono) e o FastAPI as executa num pool de threads, como o Tomcat. |
Como esta API segue o mesmo contrato da API Spring (/api/tarefas, campos id, descricao e concluida) e tem CORS liberado, os frontends da trilha funcionam com ela. Eles vêm configurados para a API Spring na porta 8080; para usar esta API, troque a URL no arquivo indicado para a porta 8000:
| Cliente | Arquivo a editar (pasta web-* do projeto) |
Valor novo | Rota usada para marcar como concluída |
|---|---|---|---|
| Angular | web-angular/src/app/services/tarefa.service.ts (apiUrl) |
http://localhost:8000/api/tarefas |
PUT /api/tarefas/{id} |
| Svelte | web-svelte/src/services/tarefaService.js (API_URL) |
http://localhost:8000/api/tarefas |
PATCH /api/tarefas/{id}/status |
| Vue.js | web-vue/src/services/tarefaService.js (ou a variável VITE_API_URL) |
http://localhost:8000/api |
PUT /api/tarefas/{id} |
| React | web-react/src/services/api.js (baseURL) |
http://localhost:8000/api |
PATCH /api/tarefas/{id}/status |
Os clientes mobile dos mesmos projetos (catálogo) seguem a mesma ideia; no emulador Android, localhost vira 10.0.2.2.