📚 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 pasta api-fastapi.

✅ Testado em: Windows 11 com Python 3.11, 3.13 e 3.14, usando as versões fixadas no requirements.txt.

Diagrama de arquitetura do projeto gerado do código

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).


📐 1. Engenharia de Software: Levantamento de Requisitos e Rastreabilidade

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.

📝 1.1 Contexto de Negócio e Histórias de Usuário (User Stories)

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:


📋 1.2 Especificação de Requisitos Funcionais (RF)

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.

🛡️ 1.3 Especificação de Requisitos Não-Funcionais (RNF)

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.

🎭 1.4 Diagrama de Casos de Uso (UML Use Case)

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

🗺️ 1.5 Matriz de Rastreabilidade de Requisitos (RTM)

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

🗺️ 2. Arquitetura da Aplicação e Visão Sistêmica

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

Arquitetura em Camadas do FastAPI


🧠 3. Teoria Descomplicada: O que você precisa saber

Antes de abrir o código, vamos esclarecer os quatro conceitos essenciais que fundamentam o ecossistema FastAPI.

🍽️ A Analogia do Restaurante: Como Funciona uma API com FastAPI?

Imagine um restaurante moderno:

A Analogia do Restaurante no FastAPI

📦 O que é JSON e a Serialização Automática do Pydantic?

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 id aparece por último porque TarefaResponse herda descricao e concluida de TarefaBase e só depois declara id. Em JSON a ordem das chaves não muda o significado.

🚦 Os Verbos HTTP do CRUD e a Operação de Toggle

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

⚡ Ciclo de Vida da Requisição HTTP no FastAPI

Abaixo vemos o pipeline percorrido desde a chegada do pacote HTTP via ASGI até a persistência no banco relacional:

Ciclo de Vida da Requisição no FastAPI

🔄 Tabela Comparativa: Java (Spring Boot) ➔ Python (FastAPI)

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

⚙️ 4. Construção do Backend Passo a Passo (Tutorial Autoguiado)

Este tutorial foi estruturado especificamente para execução no Windows, com orientações completas para Visual Studio Code e JetBrains PyCharm.

💻 4.1 Preparação do Ambiente no Windows

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 --version abrir a Microsoft Store ou disser que o comando não existe, use o inicializador py que vem com o instalador oficial: py --version (e, nos comandos abaixo, py -m venv .venv no lugar de python -m venv .venv). Depois que o ambiente virtual estiver ativo, python passa 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

🛠️ 4.2 Como Abrir e Configurar o Projeto nas IDEs

ℹ️ A pasta api-fastapi e o ambiente .venv sã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.

No Visual Studio Code:

  1. Abra o VS Code e vá em File > Open Folder… (ou Ctrl+K Ctrl+O).
  2. Selecione a pasta api-fastapi (a que você criou no Passo 0 ou, no código pronto, catalogo_projetos\python_web_tarefas_01_fastapi\api-fastapi).
  3. Pressione Ctrl+Shift+P, digite Python: Select Interpreter e escolha o interpretador do ambiente virtual .venv criado.
  4. Abra um terminal integrado no VS Code com Ctrl+\ (o VS Code ativará o (.venv)` automaticamente no rodapé do terminal).

No JetBrains PyCharm:

  1. Abra o PyCharm e selecione Open.
  2. Navegue até a pasta api-fastapi (a do Passo 0 ou a do código pronto) e abra como projeto.
  3. Vá em File > Settings > Project: api-fastapi > Python Interpreter.
  4. Clique no ícone de engrenagem ⚙️ $\rightarrow$ Add Interpreter > Add Local Interpreter… $\rightarrow$ selecione Existing Environment e aponte para o caminho .venv\Scripts\python.exe.
  5. No painel de navegação à esquerda, clique com o botão direito sobre a pasta raiz 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.

📂 Passo 0: Criar a Pasta e a Estrutura do Projeto

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):

Código: app/__init__.py

# Módulo de Inicialização da Aplicação

Código: app/models/__init__.py

from app.models.tarefa import Tarefa

__all__ = ["Tarefa"]

Código: app/schemas/__init__.py

from app.schemas.tarefa import TarefaCreate, TarefaUpdate, TarefaStatusUpdate, TarefaResponse

__all__ = ["TarefaCreate", "TarefaUpdate", "TarefaStatusUpdate", "TarefaResponse"]

Código: app/repositories/__init__.py

from app.repositories.tarefa_repository import TarefaRepository

__all__ = ["TarefaRepository"]

Código: app/services/__init__.py

from app.services.tarefa_service import TarefaService

__all__ = ["TarefaService"]

Código: app/routers/__init__.py

from app.routers.tarefas import router as tarefas_router

__all__ = ["tarefas_router"]

Código: tests/__init__.py

# Pacote de Testes da API de Tarefas

⚠️ Os __init__.py das camadas importam arquivos que você só vai escrever nos Passos 3 a 7. Até lá, o Python acusaria ModuleNotFoundError se 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:

Código: .gitignore

.venv/
__pycache__/
*.pyc
*.db
.pytest_cache/
.coverage
htmlcov/
.env
.DS_Store

📦 Passo 1: Dependências do Projeto (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.

Código: requirements.txt

fastapi==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 starlette nã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:

Código: 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:

Código: 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


⚙️ Passo 2: Configuração e Conexão com o Banco (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.

Código: 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()

Código: 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
# 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()

Código: .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


📝 Passo 3: Modelagem Relacional ORM (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)"
    }

Código: 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)

🎯 Checkpoint de Verificação 3: Mapeamento Objeto-Relacional (ORM)


🛡️ Passo 4: Schemas DTO e Validação Declarativa (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

Código: app/schemas/tarefa.py

from 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


🗄️ Passo 5: Camada de Repositório (repositories/tarefa_repository.py)

O repositório é responsável exclusivamente por executar operações no banco de dados via SQLAlchemy.

Código: 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()

    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


🧠 Passo 6: Camada de Serviço e Regras de Negócio (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.

Código: 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)

    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


🔌 Passo 7: O Controlador REST (routers/tarefas.py)

O roteador expõe os endpoints HTTP utilizando decorators semânticos e injeção de dependências nativa via Depends().

Código: 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, 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


🚀 Passo 8: Aplicação Principal, CORS e Lifespan (main.py)

O arquivo app/main.py reúne os middlewares, a configuração do OpenAPI e o ciclo de vida da aplicação.

Código: 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 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


🧪 5. Testes Automatizados com 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.

Código: 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()

Código: 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):
    # 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.


🌐 6. Dominando o Swagger UI Interativo a Fundo (/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!

Dominando o Swagger UI Interativo

📖 6.1 Como Acessar as Interfaces de Documentação

Com a aplicação rodando via Uvicorn na porta 8000:


🎨 6.2 Anatomia e Código de Cores dos Verbos no Swagger UI

No Swagger UI, os métodos HTTP são categorizados por cores universais:


🧪 6.3 Bateria de Homologação Passo a Passo no Swagger UI

Siga esta sequência prática no Swagger UI para homologar visualmente todos os Requisitos Funcionais:

1️⃣ Teste GET — Listagem Inicial (Validação do RF-02)

  1. Localize a rota azul GET /api/tarefas e clique nela para expandir.
  2. Clique no botão “Try it out” no canto superior direito do bloco.
  3. Clique no botão azul “Execute”.
  4. Inspecione o resultado em Server response:
    • Code: 200
    • Response body: [] (array vazio, confirmando que a base de dados iniciou limpa).

2️⃣ Teste POST — Criar a Primeira Tarefa (Validação do RF-01)

  1. Expanda a rota verde POST /api/tarefas.
  2. Clique em “Try it out”.
  3. O Swagger preencherá automaticamente o campo Request body com o exemplo que definimos no Pydantic!
  4. Edite ou confirme o JSON:
    {
      "descricao": "Estudar fundamentos de FastAPI e Uvicorn",
      "concluida": false
    }
    
  5. Clique em “Execute”.
  6. Resultado Esperado:
    • Code: 201
    • Response body:
      {
        "descricao": "Estudar fundamentos de FastAPI e Uvicorn",
        "concluida": false,
        "id": 1
      }
      

3️⃣ Teste POST — Criar a Segunda Tarefa (Validação do RF-01)

  1. Ainda no endpoint POST /api/tarefas, altere o Request body para:
    {
      "descricao": "Dominar a documentação interativa Swagger UI",
      "concluida": false
    }
    
  2. Clique em “Execute”.
  3. Resultado Esperado: Código 201 e retorno com "id": 2.

4️⃣ Teste GET — Listar Novamente com Dados (Validação do RF-02)

  1. Volte ao endpoint GET /api/tarefas e clique em “Execute”.
  2. Resultado Esperado: Código 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
      }
    ]
    

5️⃣ Teste GET /{id} — Consulta Pontual e Teste do Erro 404 (Validação do RF-03)

  1. Expanda o endpoint azul GET /api/tarefas/{id}.
  2. Clique em “Try it out”.
  3. No campo de parâmetro id, digite 1 e clique em “Execute”.
    • Resultado: Código 200 com os dados da Tarefa 1.
  4. Agora digite um ID inexistente, por exemplo 999, e clique em “Execute”:
    • Resultado: Código 404 Not Found com a mensagem semântica {"detail": "Tarefa não encontrada com o id: 999"}.

6️⃣ Teste PATCH /{id}/toggle — Alternar Conclusão (Validação do RF-05)

  1. Expanda o endpoint verde-água PATCH /api/tarefas/{id}/toggle.
  2. Clique em “Try it out”.
  3. Informe id: 1 e clique em “Execute”.
  4. Observe o retorno: a Tarefa 1 agora possui "concluida": true!
  5. Clique em “Execute” novamente: a mesma tarefa agora volta para "concluida": false. Cada chamada inverte o valor atual.

6️⃣➕ Teste PATCH /{id}/status — Definir Conclusão (Validação do RF-05)

  1. Expanda o endpoint PATCH /api/tarefas/{id}/status e clique em “Try it out”.
  2. Informe id: 1 e use o corpo de exemplo {"concluida": true}.
  3. Clique em “Execute” duas vezes: as duas respostas trazem "concluida": true. Diferente do toggle, repetir o pedido não desfaz a marcação.

7️⃣ Teste PUT /{id} — Atualização (Validação do RF-04)

  1. Expanda o endpoint laranja PUT /api/tarefas/{id}.
  2. Clique em “Try it out”, informe id: 1 e cole no corpo:
    {
      "descricao": "Estudar fundamentos de FastAPI e Uvicorn (Revisado)",
      "concluida": true
    }
    
  3. Clique em “Execute”.
  4. Resultado: Código 200 com a descrição atualizada e status refletido.
  5. Experimente enviar só {"concluida": false}: a descrição continua a mesma, porque campos omitidos não são alterados.

8️⃣ Teste de Validação Fail-Fast Pydantic — Erro 422 (Validação do RNF-05)

  1. No endpoint POST /api/tarefas, experimente enviar uma descrição vazia:
    {
      "descricao": "",
      "concluida": false
    }
    
  2. Clique em “Execute”.
  3. Observe a resposta: Código 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).

9️⃣ Teste DELETE /{id} — Exclusão Permanente (Validação do RF-06)

  1. Expanda o endpoint vermelho DELETE /api/tarefas/{id}.
  2. Clique em “Try it out”, digite id: 2 e clique em “Execute”.
  3. Resultado: Código 204 No Content (sem corpo, comprovando exclusão bem-sucedida).
  4. Consulte a rota GET /api/tarefas: apenas a Tarefa 1 permanece na lista.

📊 6.4 Matriz de Homologação & Checkpoints de Validação da API REST

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 ✅

🌳 7. Estrutura Completa do Projeto Consolidado

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:


🚀 8. Como Executar no Laboratório

Você pode executar o servidor de desenvolvimento de quatro formas simples, de acordo com o seu fluxo de trabalho preferido:

Opção 1: No Visual Studio Code (Terminal Integrado PowerShell) — Mais Prático

  1. Abra a pasta api-fastapi no VS Code.
  2. Certifique-se de que o interpretador .venv está selecionado (canto inferior direito ou via Ctrl+Shift+P > Python: Select Interpreter).
  3. Abra o terminal integrado (Ctrl+\`) e inicie o servidor com auto-reload:
    uvicorn app.main:app --reload --port 8000
    
  4. Para parar o servidor, pressione Ctrl+C no terminal.

Opção 2: No JetBrains PyCharm (Execução Visual Direta)

  1. Abra o arquivo app/main.py no PyCharm.
  2. Certifique-se de que a pasta api-fastapi está marcada como Sources Root (clique direito na pasta $\rightarrow$ Mark Directory as > Sources Root).
  3. No terminal do PyCharm com o interpretador ativo, execute:
    uvicorn app.main:app --reload --port 8000
    
  4. Se preferir criar uma configuração visual de execução: vá em Run > Edit Configurations… $\rightarrow$ clique em + $\rightarrow$ selecione FastAPI (só no PyCharm Professional) e aponte para 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).

Opção 3: No Terminal do Windows / PowerShell

  1. Abra o PowerShell e navegue até a pasta 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
    
  2. Ative o ambiente virtual:
    .\.venv\Scripts\Activate.ps1
    
  3. Inicie o servidor:
    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!


Opção 4: Com Docker (Containerização)

ℹ️ 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:

Código: 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}"]

Código: .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.db do container fica dentro dele: ao remover o container, as tarefas somem. Para guardar os dados, use um volume ou um PostgreSQL (variável DATABASE_URL).


🧯 9. Se der errado (Resolução de Problemas Comuns / Troubleshooting)

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.

🌉 10. Comparativo Didático e A Ponte para os Próximos Passos

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.

👉 Próximo Passo: Integrando a API com Frontends Desacoplados

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.