📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Backend com Python e FastAPI. Recomendado revisar antes de começar.

🚀 Projeto Lista de Tarefas (To-Do List) com Python e FastAPI

v1.0`

🗺️ Visão Geral da Arquitetura

Neste projeto, construiremos uma API REST central e desacoplada para um sistema de “Lista de Tarefas” (To-Do List), utilizando a stack moderna de Python 3.11+, FastAPI, SQLAlchemy 2.0 e Pydantic v2.

A API funciona como a única fonte de verdade para qualquer frontend (Angular, React, Vue, HTMX, Mobile ou Desktop):

Diagrama da Arquitetura

flowchart TD
    subgraph "📱 Clientes (Frontends)"
    Web["💻 Frontend Web<br>(Angular / React / Vue / HTMX)"]
    Desktop["🖥️ Frontend Desktop<br>(Custom UI)"]
    Mobile["📱 Frontend Mobile<br>(Android / Flutter / Ionic)"]
    end

    subgraph "⚙️ Serviços (Backend Python)"
    API["🔌 Backend API REST<br>(FastAPI + Uvicorn)"]
    DB[("🗄️ Banco de Dados<br>SQLite (Dev) / PostgreSQL (Prod)")]
    end

    %% Conexões de Dados
    Web -->|"HTTP / JSON (CORS)"| API
    Desktop -->|HTTP / JSON| API
    Mobile -->|HTTP / JSON| API
    API --- DB

    %% Estilização
    style API fill:#dfd,stroke:#333,stroke-width:2px
    style DB fill:#def,stroke:#333,stroke-width:2px
    style Web fill:#ffe,stroke:#333,stroke-width:2px

—`

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

Camada / Papel Stack Java (listadetarefas_01) Stack Python (listadetarefas_python_01)
Linguagem & Runtime Java 17+ (JDK) Python 3.11+
Framework Web Spring Boot 3.x (@RestController) FastAPI (APIRouter, auto docs Swagger)
Acesso a Dados Spring Data JPA / Hibernate SQLAlchemy 2.0 (Mapped, mapped_column)
DTOs & Validação Lombok / Bean Validation Pydantic v2 (BaseModel, Field)
Injeção de Dependências @Autowired Depends()
Banco de Dados (Dev) H2 em memória SQLite (sqlite:///./tarefas.db)
Banco de Dados (Prod) PostgreSQL PostgreSQL (Neon) via psycopg
Testes Spring Boot Test / JUnit pytest + httpx (TestClient)
Documentação API SpringDoc / Swagger Swagger UI nativo em /docs

—`

⚙️ Módulo 1: A Fundação – Backend com FastAPI (listadetarefas-api)

Objetivo: Criar o serviço central RESTful em camadas que gerencia o ciclo de vida das tarefas (CRUD completo).

🛠️ Ferramentas Necessárias


📂 Passo 1: Criação e Estrutura do Projeto

  1. Crie a pasta do projeto e inicialize o ambiente virtual:
mkdir listadetarefas-api
cd listadetarefas-api
git init
git branch -M main

# Criar ambiente virtual Python 3.11+
python -m venv .venv

# Ativar o ambiente virtual:
# Windows (PowerShell):
.venv\Scripts\Activate.ps1
# Linux/macOS:
source .venv/bin/activate
  1. Crie o arquivo requirements.txt:
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
sqlalchemy>=2.0.35
pydantic>=2.9.0
pydantic-settings>=2.5.0
psycopg[binary]>=3.2.0
pytest>=8.3.0
httpx>=0.27.0

Instale as dependências:

pip install -r requirements.txt

Estrutura Final de Pastas da API

listadetarefas-api/
├── app/
│   ├── __init__.py
│   ├── config.py                  # Configurações e CORS
│   ├── database.py                # Engine SQLAlchemy e SessionLocal
│   ├── models/
│   │   ├── __init__.py
│   │   └── tarefa.py              # Entidade SQLAlchemy (tb_tarefas)
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── tarefa.py              # DTOs Pydantic (Create, Update, Response)
│   ├── repositories/
│   │   ├── __init__.py
│   │   └── tarefa_repository.py   # Operações no banco (find, save, delete)
│   ├── services/
│   │   ├── __init__.py
│   │   └── tarefa_service.py      # Regras de negócio e exceções
│   ├── routers/
│   │   ├── __init__.py
│   │   └── tarefas.py             # Endpoints HTTP REST
│   └── main.py                    # App FastAPI, CORS e Lifespan
├── tests/
│   ├── __init__.py
│   ├── conftest.py                # Fixtures de teste em memória
│   └── test_tarefas_api.py        # Testes de integração pytest
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── README.md
└── requirements.txt

⚙️ Passo 2: Configuração e Conexão com o Banco de Dados

Arquivo: app/config.py

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    APP_NAME: str = "API Lista de Tarefas"
    DATABASE_URL: str = "sqlite:///./tarefas.db"
    ENVIRONMENT: str = "dev"
    CORS_ORIGINS: list[str] = ["*"]

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore"
    )

settings = Settings()

Arquivo: app/database.py

from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker, Session
from app.config import settings

db_url = settings.DATABASE_URL
if db_url.startswith("postgres://"):
    db_url = db_url.replace("postgres://", "postgresql+psycopg://", 1)
elif db_url.startswith("postgresql://") and not db_url.startswith("postgresql+"):
    db_url = db_url.replace("postgresql://", "postgresql+psycopg://", 1)

connect_args = {"check_same_thread": False} if db_url.startswith("sqlite") else {}

engine = create_engine(db_url, connect_args=connect_args)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db() -> Generator[Session, None, None]:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

📝 Passo 3: Modelagem dos Dados e Schemas DTO

Diagrama Entidade-Relacionamento (ER)

erDiagram
    TB_TAREFAS {
        INT id PK "Auto-incremento"
        VARCHAR descricao "Descrição da tarefa"
        BOOLEAN concluida "Status de conclusão"
    }

Arquivo: app/models/tarefa.py

from sqlalchemy import String, Boolean
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base

class Tarefa(Base):
    __tablename__ = "tb_tarefas"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    descricao: Mapped[str] = mapped_column(String(255), nullable=False)
    concluida: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

Arquivo: app/schemas/tarefa.py

from pydantic import BaseModel, ConfigDict, Field

class TarefaBase(BaseModel):
    descricao: str = Field(..., min_length=1, max_length=255, description="Descrição da tarefa")
    concluida: bool = Field(default=False, description="Status de conclusão")

class TarefaCreate(TarefaBase):
    pass

class TarefaUpdate(BaseModel):
    descricao: str | None = Field(default=None, min_length=1, max_length=255)
    concluida: bool | None = Field(default=None)

class TarefaResponse(TarefaBase):
    id: int

    model_config = ConfigDict(from_attributes=True)

🏗️ Passo 4: Construção da Arquitetura em Camadas

Diagrama de Classes

classDiagram
    direction LR
    TarefasRouter ..> TarefaService : Usa
    TarefaService ..> TarefaRepository : Usa
    TarefaRepository ..> Tarefa : Gerencia

    class TarefasRouter {
        +listar_tarefas() List~TarefaResponse~
        +buscar_tarefa(id: int) TarefaResponse
        +criar_tarefa(tarefa: TarefaCreate) TarefaResponse
        +atualizar_tarefa(id: int, tarefa: TarefaUpdate) TarefaResponse
        +deletar_tarefa(id: int) void
    }

    class TarefaService {
        +listar_todas() List~Tarefa~
        +buscar_por_id(id: int) Tarefa
        +criar(tarefa_in: TarefaCreate) Tarefa
        +atualizar(id: int, tarefa_in: TarefaUpdate) Tarefa
        +deletar(id: int) void
    }

    class TarefaRepository {
        +find_all() List~Tarefa~
        +find_by_id(id: int) Tarefa
        +create(tarefa_in: TarefaCreate) Tarefa
        +update(db_tarefa: Tarefa, tarefa_in: TarefaUpdate) Tarefa
        +delete(db_tarefa: Tarefa) void
    }

    class Tarefa {
        +int id
        +str descricao
        +bool concluida
    }

Arquivo: app/repositories/tarefa_repository.py

from sqlalchemy.orm import Session
from app.models.tarefa import Tarefa
from app.schemas.tarefa import TarefaCreate, TarefaUpdate

class TarefaRepository:
    def __init__(self, db: Session):
        self.db = db

    def find_all(self) -> list[Tarefa]:
        return self.db.query(Tarefa).order_by(Tarefa.id.asc()).all()

    def find_by_id(self, id: int) -> Tarefa | None:
        return self.db.query(Tarefa).filter(Tarefa.id == id).first()

    def create(self, tarefa_in: TarefaCreate) -> Tarefa:
        db_tarefa = Tarefa(
            descricao=tarefa_in.descricao,
            concluida=tarefa_in.concluida,
        )
        self.db.add(db_tarefa)
        self.db.commit()
        self.db.refresh(db_tarefa)
        return db_tarefa

    def update(self, db_tarefa: Tarefa, tarefa_in: TarefaUpdate) -> Tarefa:
        if tarefa_in.descricao is not None:
            db_tarefa.descricao = tarefa_in.descricao
        if tarefa_in.concluida is not None:
            db_tarefa.concluida = tarefa_in.concluida
        
        self.db.commit()
        self.db.refresh(db_tarefa)
        return db_tarefa

    def delete(self, db_tarefa: Tarefa) -> None:
        self.db.delete(db_tarefa)
        self.db.commit()

Arquivo: app/services/tarefa_service.py

from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.repositories.tarefa_repository import TarefaRepository
from app.schemas.tarefa import TarefaCreate, TarefaUpdate
from app.models.tarefa import Tarefa

class TarefaService:
    def __init__(self, db: Session):
        self.repository = TarefaRepository(db)

    def listar_todas(self) -> list[Tarefa]:
        return self.repository.find_all()

    def buscar_por_id(self, id: int) -> Tarefa:
        tarefa = self.repository.find_by_id(id)
        if not tarefa:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Tarefa não encontrada com o id: {id}",
            )
        return tarefa

    def criar(self, tarefa_in: TarefaCreate) -> Tarefa:
        return self.repository.create(tarefa_in)

    def atualizar(self, id: int, tarefa_in: TarefaUpdate) -> Tarefa:
        db_tarefa = self.buscar_por_id(id)
        return self.repository.update(db_tarefa, tarefa_in)

    def deletar(self, id: int) -> None:
        db_tarefa = self.buscar_por_id(id)
        self.repository.delete(db_tarefa)

Arquivo: app/routers/tarefas.py

from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.schemas.tarefa import TarefaCreate, TarefaUpdate, TarefaResponse
from app.services.tarefa_service import TarefaService

router = APIRouter(prefix="/api/tarefas", tags=["Tarefas"])

def get_tarefa_service(db: Session = Depends(get_db)) -> TarefaService:
    return TarefaService(db)

@router.get("", response_model=list[TarefaResponse], summary="Listar todas as tarefas")
def listar_tarefas(service: TarefaService = Depends(get_tarefa_service)):
    return service.listar_todas()

@router.get("/{id}", response_model=TarefaResponse, summary="Buscar tarefa por ID")
def buscar_tarefa(id: int, service: TarefaService = Depends(get_tarefa_service)):
    return service.buscar_por_id(id)

@router.post("", response_model=TarefaResponse, status_code=status.HTTP_201_CREATED, summary="Criar nova tarefa")
def criar_tarefa(tarefa_in: TarefaCreate, service: TarefaService = Depends(get_tarefa_service)):
    return service.criar(tarefa_in)

@router.put("/{id}", response_model=TarefaResponse, summary="Atualizar tarefa existente")
def atualizar_tarefa(id: int, tarefa_in: TarefaUpdate, service: TarefaService = Depends(get_tarefa_service)):
    return service.atualizar(id, tarefa_in)

@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT, summary="Deletar tarefa por ID")
def deletar_tarefa(id: int, service: TarefaService = Depends(get_tarefa_service)):
    service.deletar(id)

Arquivo: app/main.py

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.database import Base, engine
from app.routers import tarefas_router

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Inicializa as tabelas do banco
    Base.metadata.create_all(bind=engine)
    yield

app = FastAPI(
    title=settings.APP_NAME,
    description="API RESTful de Lista de Tarefas com FastAPI e SQLAlchemy",
    version="1.0.0",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(tarefas_router)

✅ Passo 5: Testes Automatizados com pytest

Arquivo: tests/conftest.py

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app.main import app
from app.database import Base, get_db

SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False},
    poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture(scope="function")
def db_session():
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()
        Base.metadata.drop_all(bind=engine)

@pytest.fixture(scope="function")
def client(db_session):
    def override_get_db():
        try:
            yield db_session
        finally:
            pass

    app.dependency_overrides[get_db] = override_get_db
    with TestClient(app) as test_client:
        yield test_client
    app.dependency_overrides.clear()

Arquivo: tests/test_tarefas_api.py

def test_listar_tarefas_vazio(client):
    response = client.get("/api/tarefas")
    assert response.status_code == 200
    assert response.json() == []

def test_criar_tarefa(client):
    payload = {"descricao": "Estudar FastAPI", "concluida": False}
    response = client.post("/api/tarefas", json=payload)
    assert response.status_code == 201
    data = response.json()
    assert data["id"] == 1
    assert data["descricao"] == "Estudar FastAPI"
    assert data["concluida"] is False

def test_buscar_tarefa_por_id(client):
    create_resp = client.post("/api/tarefas", json={"descricao": "Comprar pão", "concluida": False})
    tarefa_id = create_resp.json()["id"]

    response = client.get(f"/api/tarefas/{tarefa_id}")
    assert response.status_code == 200
    assert response.json()["descricao"] == "Comprar pão"

    response_404 = client.get("/api/tarefas/999")
    assert response_404.status_code == 404

def test_atualizar_tarefa(client):
    create_resp = client.post("/api/tarefas", json={"descricao": "Lavar o carro", "concluida": False})
    tarefa_id = create_resp.json()["id"]

    update_resp = client.put(f"/api/tarefas/{tarefa_id}", json={"descricao": "Lavar o carro e aspirar", "concluida": True})
    assert update_resp.status_code == 200
    assert update_resp.json()["descricao"] == "Lavar o carro e aspirar"
    assert update_resp.json()["concluida"] is True

def test_deletar_tarefa(client):
    create_resp = client.post("/api/tarefas", json={"descricao": "Tarefa temporária", "concluida": False})
    tarefa_id = create_resp.json()["id"]

    delete_resp = client.delete(f"/api/tarefas/{tarefa_id}")
    assert delete_resp.status_code == 204

    get_resp = client.get(f"/api/tarefas/{tarefa_id}")
    assert get_resp.status_code == 404

Execute os testes:

pytest -v

Inicie o servidor de desenvolvimento:

uvicorn app.main:app --reload --port 8000

🌐 Passo 6: Documentação Interativa Swagger UI

O FastAPI gera automaticamente a documentação OpenAPI 3.1 e disponibiliza interfaces interativas para testar a API diretamente no navegador:

📖 Como Usar o Swagger UI Passo a Passo:

  1. Acessar o Swagger: Com o servidor rodando (uvicorn app.main:app --reload), abra http://127.0.0.1:8000/docs no navegador.

  2. Criar uma Tarefa (POST /api/tarefas):
    • Clique no endpoint verde POST /api/tarefas.
    • Clique em “Try it out”.
    • No corpo da requisição (Request body), insira o JSON:
      {
        "descricao": "Estudar FastAPI e Swagger",
        "concluida": false
      }
      
    • Clique em “Execute”.
    • Verifique a resposta com código 201 Created e o objeto com o id gerado (ex: {"id": 1, ...}).
  3. Listar Todas as Tarefas (GET /api/tarefas):
    • Clique no endpoint azul GET /api/tarefas.
    • Clique em “Try it out”“Execute”.
    • O Swagger exibirá o array JSON com todas as tarefas cadastradas (Status 200 OK).
  4. Buscar Tarefa por ID (GET /api/tarefas/{id}):
    • Clique em GET /api/tarefas/{id}“Try it out”.
    • Informe o parâmetro id: 1 e clique em “Execute”.
    • Experimente digitar um ID inexistente (ex: 999) e veja o retorno 404 Not Found.
  5. Atualizar Tarefa (PUT /api/tarefas/{id}):
    • Clique em PUT /api/tarefas/{id}“Try it out”.
    • Informe id: 1 e no corpo:
      {
        "descricao": "Estudar FastAPI e Swagger (Concluído)",
        "concluida": true
      }
      
    • Clique em “Execute” e observe os campos atualizados no retorno 200 OK.
  6. Deletar Tarefa (DELETE /api/tarefas/{id}):
    • Clique no endpoint vermelho DELETE /api/tarefas/{id}“Try it out”.
    • Informe id: 1 e clique em “Execute”.
    • O status code retornado será 204 No Content.

🐳 Passo 7: Containerização com Docker

Arquivo: Dockerfile

FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV PORT=8000
EXPOSE 8000

CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]

🎯 Conclusão

Você concluiu a construção da API RESTful de Lista de Tarefas em Python 3.11+, utilizando FastAPI, SQLAlchemy 2.0, arquitetura em camadas e cobertura completa de testes automatizados com pytest.

Esta API está pronta para ser consumida por qualquer cliente Frontend (Angular, React, Vue, Mobile ou HTMX).

—`

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd python_web_tarefas_01_fastapi

2. Execute a aplicação e os testes

pip install -r requirements.txt
uvicorn app:app --reload
# ou python main.py

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/python_web_tarefas_01_fastapi