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

🎮 Biblioteca de Jogos 01 (Python) — CRUD Básico

v1.1

Trilha de Aprendizado — Projeto 1 de 4 (Trilha Python)

🎓 Nível profissional simulado: Estagiário / Júnior. Primeiro contato com Python 3.11+, FastAPI, SQLAlchemy 2.0 e Jinja2 + Bootstrap 5: uma única entidade (Jogo), endpoints web com formulário e redirecionamentos, e endpoints REST JSON documentados interativamente pelo Swagger UI (/docs). O objetivo é dominar o ciclo de vida de uma aplicação web completa, da persistência ao deploy. (Inclui testes automatizados com pytest e Swagger interativo).

🗺️ Mapa da trilha: veja a tabela comparativa na listagem principal de projetos.


🎯 Objetivo

Construir uma aplicação web e API para gerenciar o catálogo da sua coleção pessoal de jogos: cadastrar novos títulos, listar a biblioteca com badges de status (“Zerado” / “Jogando”), alternar status e apagar jogos.

flowchart TD
    subgraph Browser ["🌐 Navegador do Usuário"]
        UI["💻 Interface Web (Bootstrap 5)"]
        SW["📖 Swagger UI (/docs)"]
    end

    subgraph App ["⚡ Aplicação FastAPI (Docker)"]
        Routes["Rotas Web & REST (routes.py)"]
        DBLayer["Sessão SQLAlchemy (database.py)"]
        Model["Entidade Jogo (models.py)"]
    end

    subgraph Database ["🗄️ Banco de Dados"]
        DB[("SQLite (Dev) / PostgreSQL Neon (Prod)")]
    end

    UI -->|HTML Forms / POST| Routes
    SW -->|HTTP / JSON| Routes
    Routes --> DBLayer
    DBLayer --> Model
    Model --- DB

🔄 Tabela Comparativa: Java ➔ Python

Papel Stack Java (javaweb_jogos_01_crud) Stack Python (bibliotecajogos_python_01)
Linguagem & Runtime Java 21 (LTS) Python 3.11+
Framework Web Spring Boot 3.x / 4.x FastAPI + Uvicorn
ORM / Mapeamento Spring Data JPA / Hibernate SQLAlchemy 2.0 (Mapped, mapped_column)
Template Engine Thymeleaf (th:each, th:if) Jinja2 ({% for %}, {% if %})
Frontend UI Bootstrap 5 Bootstrap 5
Banco Local (Dev) H2 Database (em memória) SQLite (sqlite:///./bibliotecajogos.db)
Banco Remoto (Prod) PostgreSQL (Neon) PostgreSQL (Neon) via psycopg
Documentação da API SpringDoc / Swagger Swagger UI nativo em /docs
Testes Automatizados JUnit 5 pytest + httpx (TestClient)
Deploy Docker + Render Docker + Render

🗄️ Modelagem de Dados

erDiagram
    JOGOS {
        INT id PK "Auto-incremento"
        VARCHAR titulo "Título do jogo"
        VARCHAR plataforma "Plataforma (PC, PS5, Xbox, Switch)"
        BOOLEAN concluido "Status (True: Zerado / False: Jogando)"
    }

🗂️ Estrutura do Projeto

bibliotecajogos_python_01/
├── app/
│   ├── __init__.py
│   ├── config.py             # Configurações com pydantic-settings (.env / prod)
│   ├── database.py           # Engine SQLAlchemy 2.0 e SessionLocal
│   ├── models.py             # Entidade Jogo (tabela jogos)
│   ├── schemas.py            # Schemas Pydantic para validação e Swagger
│   ├── routes.py             # Rotas Web (Bootstrap 5) e Endpoints REST
│   ├── main.py               # Instância FastAPI e lifespan
│   └── templates/
│       └── jogos.html        # Interface visual responsiva em Bootstrap 5
├── tests/
│   ├── __init__.py
│   ├── conftest.py           # Fixture com banco SQLite em memória isolado
│   └── test_jogos.py         # Testes de integração Web e REST
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile                # Imagem baseada em python:3.11-slim
├── README.md                 # Instruções de execução e Swagger
└── requirements.txt          # Dependências do projeto

⚙️ Passo 1: Configuração Inicial e Dependências

  1. Crie o diretório do projeto e configure o ambiente virtual:
mkdir bibliotecajogos-py
cd bibliotecajogos-py
git init
git branch -M main

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

# Ativar 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
jinja2>=3.1.4
python-multipart>=0.0.9
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

💾 Passo 2: Configuração e Banco de Dados

Arquivo: app/config.py

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    APP_NAME: str = "Biblioteca de Jogos"
    DATABASE_URL: str = "sqlite:///./bibliotecajogos.db"
    ENVIRONMENT: str = "dev"

    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: Modelos e Schemas

Arquivo: app/models.py

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

class Jogo(Base):
    __tablename__ = "jogos"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    titulo: Mapped[str] = mapped_column(String(255), nullable=False)
    plataforma: Mapped[str] = mapped_column(String(100), nullable=False)
    concluido: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

Arquivo: app/schemas.py

from pydantic import BaseModel, ConfigDict, Field

class JogoBase(BaseModel):
    titulo: str = Field(..., min_length=1, max_length=255, description="Título do jogo")
    plataforma: str = Field(..., min_length=1, max_length=100, description="Plataforma (ex: PC, PS5, Xbox, Switch)")
    concluido: bool = Field(default=False, description="Indica se o jogo foi concluído/zerado")

class JogoCreate(JogoBase):
    pass

class JogoUpdate(BaseModel):
    titulo: str | None = Field(default=None, min_length=1, max_length=255)
    plataforma: str | None = Field(default=None, min_length=1, max_length=100)
    concluido: bool | None = Field(default=None)

class JogoResponse(JogoBase):
    id: int

    model_config = ConfigDict(from_attributes=True)

🚦 Passo 4: Rotas e Ponto de Entrada

Arquivo: app/routes.py

from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, Depends, Form, Request, HTTPException, status
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session

from app.database import get_db
from app.models import Jogo
from app.schemas import JogoCreate, JogoUpdate, JogoResponse

router = APIRouter()

TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))

# ==========================================
# Rotas Web (Jinja2 + Bootstrap 5)
# ==========================================

@router.get("/", response_class=HTMLResponse, include_in_schema=False)
def listar_web(request: Request, db: Session = Depends(get_db)):
    jogos = db.query(Jogo).order_by(Jogo.id.asc()).all()
    return templates.TemplateResponse(request=request, name="jogos.html", context={"jogos": jogos})

@router.post("/jogos", response_class=RedirectResponse, status_code=status.HTTP_303_SEE_OTHER, tags=["Web"], summary="Adicionar jogo via Formulário Web")
def adicionar_web(
    titulo: Annotated[str, Form(description="Título do jogo")],
    plataforma: Annotated[str, Form(description="Plataforma do jogo")],
    db: Session = Depends(get_db),
):
    novo_jogo = Jogo(titulo=titulo, plataforma=plataforma, concluido=False)
    db.add(novo_jogo)
    db.commit()
    return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)

@router.post("/jogos/{id}/concluir", response_class=RedirectResponse, status_code=status.HTTP_303_SEE_OTHER, tags=["Web"], summary="Alternar status do jogo via Formulário Web")
def alternar_status_web(id: int, db: Session = Depends(get_db)):
    jogo = db.query(Jogo).filter(Jogo.id == id).first()
    if jogo:
        jogo.concluido = not jogo.concluido
        db.commit()
    return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)

@router.post("/jogos/{id}/apagar", response_class=RedirectResponse, status_code=status.HTTP_303_SEE_OTHER, tags=["Web"], summary="Apagar jogo via Formulário Web")
def apagar_web(id: int, db: Session = Depends(get_db)):
    jogo = db.query(Jogo).filter(Jogo.id == id).first()
    if jogo:
        db.delete(jogo)
        db.commit()
    return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)

# ==========================================
# Endpoints REST API (Swagger / JSON)
# ==========================================

@router.get("/api/jogos", response_model=list[JogoResponse], tags=["API REST - Jogos"], summary="Listar todos os jogos")
def api_listar_jogos(db: Session = Depends(get_db)):
    return db.query(Jogo).order_by(Jogo.id.asc()).all()

@router.get("/api/jogos/{id}", response_model=JogoResponse, tags=["API REST - Jogos"], summary="Buscar jogo por ID")
def api_buscar_jogo(id: int, db: Session = Depends(get_db)):
    jogo = db.query(Jogo).filter(Jogo.id == id).first()
    if not jogo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Jogo com ID {id} não encontrado",
        )
    return jogo

@router.post("/api/jogos", response_model=JogoResponse, status_code=status.HTTP_201_CREATED, tags=["API REST - Jogos"], summary="Cadastrar novo jogo")
def api_criar_jogo(jogo_in: JogoCreate, db: Session = Depends(get_db)):
    novo_jogo = Jogo(
        titulo=jogo_in.titulo,
        plataforma=jogo_in.plataforma,
        concluido=jogo_in.concluido,
    )
    db.add(novo_jogo)
    db.commit()
    db.refresh(novo_jogo)
    return novo_jogo

@router.put("/api/jogos/{id}", response_model=JogoResponse, tags=["API REST - Jogos"], summary="Atualizar jogo por ID")
def api_atualizar_jogo(id: int, jogo_in: JogoUpdate, db: Session = Depends(get_db)):
    jogo = db.query(Jogo).filter(Jogo.id == id).first()
    if not jogo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Jogo com ID {id} não encontrado",
        )
    
    if jogo_in.titulo is not None:
        jogo.titulo = jogo_in.titulo
    if jogo_in.plataforma is not None:
        jogo.plataforma = jogo_in.plataforma
    if jogo_in.concluido is not None:
        jogo.concluido = jogo_in.concluido

    db.commit()
    db.refresh(jogo)
    return jogo

@router.patch("/api/jogos/{id}/concluir", response_model=JogoResponse, tags=["API REST - Jogos"], summary="Alternar status zerado/jogando")
def api_alternar_status(id: int, db: Session = Depends(get_db)):
    jogo = db.query(Jogo).filter(Jogo.id == id).first()
    if not jogo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Jogo com ID {id} não encontrado",
        )
    
    jogo.concluido = not jogo.concluido
    db.commit()
    db.refresh(jogo)
    return jogo

@router.delete("/api/jogos/{id}", status_code=status.HTTP_204_NO_CONTENT, tags=["API REST - Jogos"], summary="Excluir jogo por ID")
def api_deletar_jogo(id: int, db: Session = Depends(get_db)):
    jogo = db.query(Jogo).filter(Jogo.id == id).first()
    if not jogo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Jogo com ID {id} não encontrado",
        )
    db.delete(jogo)
    db.commit()

Arquivo: app/main.py

from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.config import settings
from app.database import Base, engine
from app.routes import router

@asynccontextmanager
async def lifespan(app: FastAPI):
    Base.metadata.create_all(bind=engine)
    yield

app = FastAPI(
    title="Biblioteca de Jogos API",
    description="Aplicação Web e API REST de Catálogo de Jogos em Python 3.11+, FastAPI e SQLAlchemy 2.0",
    version="1.0.0",
    lifespan=lifespan,
)

app.include_router(router)

🎨 Passo 5: Template Web (Bootstrap 5)

Arquivo: app/templates/jogos.html

<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Biblioteca de Jogos</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5" style="max-width: 900px;">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1 class="h2 mb-0">🎮 Minha Biblioteca de Jogos</h1>
        <a href="/docs" target="_blank" class="btn btn-outline-dark btn-sm">📖 Swagger UI</a>
    </div>

    <!-- Formulário de Adicionar Jogo -->
    <div class="card shadow-sm mb-4">
        <div class="card-body">
            <form action="/jogos" method="post" class="row g-2">
                <div class="col-md-5">
                    <input type="text" name="titulo" class="form-control" placeholder="Título do jogo (ex: Elden Ring)" required>
                </div>
                <div class="col-md-4">
                    <input type="text" name="plataforma" class="form-control" placeholder="Plataforma (PC, PS5, Xbox, Switch...)" required>
                </div>
                <div class="col-md-3 d-grid">
                    <button type="submit" class="btn btn-primary">Adicionar</button>
                </div>
            </form>
        </div>
    </div>

    <!-- Tabela de Jogos -->
    <div class="card shadow-sm">
        <div class="table-responsive">
            <table class="table table-hover align-middle mb-0 bg-white">
                <thead class="table-light">
                    <tr>
                        <th>Título</th>
                        <th>Plataforma</th>
                        <th>Status</th>
                        <th class="text-end">Ações</th>
                    </tr>
                </thead>
                <tbody>
                    {% for jogo in jogos %}
                    <tr>
                        <td class="fw-semibold">{{ jogo.titulo }}</td>
                        <td><span class="badge bg-light text-dark border">{{ jogo.plataforma }}</span></td>
                        <td>
                            {% if jogo.concluido %}
                            <span class="badge bg-success">Zerado</span>
                            {% else %}
                            <span class="badge bg-secondary">Jogando</span>
                            {% endif %}
                        </td>
                        <td class="text-end">
                            <form action="/jogos/{{ jogo.id }}/concluir" method="post" class="d-inline">
                                <button type="submit" class="btn btn-sm btn-outline-success">Alternar</button>
                            </form>
                            <form action="/jogos/{{ jogo.id }}/apagar" method="post" class="d-inline ms-1">
                                <button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Deseja realmente apagar este jogo?')">Apagar</button>
                            </form>
                        </td>
                    </tr>
                    {% else %}
                    <tr>
                        <td colspan="4" class="text-center text-muted py-4">Nenhum jogo cadastrado na sua biblioteca.</td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

🧪 Passo 6: 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, follow_redirects=True) as test_client:
        yield test_client
    app.dependency_overrides.clear()

Arquivo: tests/test_jogos.py

def test_web_listar_jogos_vazio(client):
    response = client.get("/")
    assert response.status_code == 200
    assert "Nenhum jogo cadastrado" in response.text

def test_web_fluxo_completo(client):
    # 1. Adicionar jogo via form
    response = client.post("/jogos", data={"titulo": "Super Mario Odyssey", "plataforma": "Nintendo Switch"})
    assert response.status_code == 200
    assert "Super Mario Odyssey" in response.text
    assert "Jogando" in response.text

    # 2. Alternar para Zerado
    response_concluir = client.post("/jogos/1/concluir")
    assert response_concluir.status_code == 200
    assert "Zerado" in response_concluir.text

    # 3. Apagar jogo
    response_apagar = client.post("/jogos/1/apagar")
    assert response_apagar.status_code == 200
    assert "Super Mario Odyssey" not in response_apagar.text

def test_api_rest_completa(client):
    # 1. Listar vazio
    resp = client.get("/api/jogos")
    assert resp.status_code == 200
    assert resp.json() == []

    # 2. Criar via POST JSON
    post_resp = client.post("/api/jogos", json={"titulo": "God of War Ragnarok", "plataforma": "PS5", "concluido": False})
    assert post_resp.status_code == 201
    jogo_id = post_resp.json()["id"]

    # 3. Buscar por ID
    get_resp = client.get(f"/api/jogos/{jogo_id}")
    assert get_resp.status_code == 200
    assert get_resp.json()["titulo"] == "God of War Ragnarok"

    # 4. Atualizar via PUT
    put_resp = client.put(f"/api/jogos/{jogo_id}", json={"titulo": "God of War (2018)", "plataforma": "PC", "concluido": True})
    assert put_resp.status_code == 200
    assert put_resp.json()["titulo"] == "God of War (2018)"

    # 5. Alternar status via PATCH
    patch_resp = client.patch(f"/api/jogos/{jogo_id}/concluir")
    assert patch_resp.status_code == 200
    assert patch_resp.json()["concluido"] is False

    # 6. Deletar via DELETE
    del_resp = client.delete(f"/api/jogos/{jogo_id}")
    assert del_resp.status_code == 204

    # 7. Buscar deletado (404)
    resp_404 = client.get(f"/api/jogos/{jogo_id}")
    assert resp_404.status_code == 404


def test_api_nao_encontrado_404(client):
    id_inexistente = 99999
    assert client.get(f"/api/jogos/{id_inexistente}").status_code == 404
    assert client.put(f"/api/jogos/{id_inexistente}", json={"titulo": "X"}).status_code == 404
    assert client.patch(f"/api/jogos/{id_inexistente}/concluir").status_code == 404
    assert client.delete(f"/api/jogos/{id_inexistente}").status_code == 404


def test_api_validacao_campos(client):
    # Título vazio não deve ser aceito (min_length=1)
    resp = client.post("/api/jogos", json={"titulo": "", "plataforma": "PC"})
    assert resp.status_code == 422

    # Plataforma ausente não deve ser aceita
    resp = client.post("/api/jogos", json={"titulo": "Zelda"})
    assert resp.status_code == 422


def test_api_atualizacao_parcial(client):
    post_resp = client.post("/api/jogos", json={"titulo": "Hollow Knight", "plataforma": "PC", "concluido": False})
    jogo_id = post_resp.json()["id"]

    # Atualiza apenas a plataforma
    put_resp = client.put(f"/api/jogos/{jogo_id}", json={"plataforma": "Switch"})
    assert put_resp.status_code == 200
    assert put_resp.json()["titulo"] == "Hollow Knight"
    assert put_resp.json()["plataforma"] == "Switch"


def test_web_acoes_jogo_inexistente(client):
    resp_concluir = client.post("/jogos/99999/concluir")
    assert resp_concluir.status_code == 200

    resp_apagar = client.post("/jogos/99999/apagar")
    assert resp_apagar.status_code == 200

Execute os testes automatizados:

pytest -v

Inicie o servidor de desenvolvimento:

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

🌐 Passo 7: Documentação Interativa com Swagger UI

O FastAPI disponibiliza documentação interativa automática para explorar os endpoints:

📖 Como Usar o Swagger UI Passo a Passo:

  1. Acesse: http://127.0.0.1:8000/docs.
  2. Cadastrar Jogo (POST /api/jogos):
    • Expanda o endpoint POST /api/jogos → Clique em “Try it out”.
    • No corpo da requisição:
      {
        "titulo": "The Witcher 3: Wild Hunt",
        "plataforma": "PC",
        "concluido": true
      }
      
    • Clique em “Execute” e confirme a criação com código 201 Created e o id retornado.
  3. Listar Todos (GET /api/jogos):
    • Expanda GET /api/jogos“Try it out”“Execute”.
  4. Alternar Status (PATCH /api/jogos/{id}/concluir):
    • Expanda o endpoint PATCH /api/jogos/{id}/concluir“Try it out”.
    • Digite o id do jogo e clique em “Execute”. O status concluido será invertido de true para false (ou vice-versa).
  5. Remover Jogo (DELETE /api/jogos/{id}):
    • Expanda DELETE /api/jogos/{id}“Try it out” → Informe o id“Execute” (Retorno: 204 No Content).

🐳 Passo 8: 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}"]

☁️ Passo 9: Deploy em Produção (Neon + Render)

  1. Crie um banco PostgreSQL gratuito no Neon e copie a connection string (ajuste o prefixo para postgresql+psycopg://).
  2. Crie um Web Service no Render apontando para o seu repositório GitHub com runtime Docker.
  3. Em Environment Variables, defina:
Variável Valor
DATABASE_URL postgresql+psycopg://neondb_owner:SENHA@ep-xxx.neon.tech/neondb?sslmode=require
ENVIRONMENT prod

🎯 Conclusão

Você implementou e publicou a aplicação Biblioteca de Jogos em Python 3.11+, FastAPI, SQLAlchemy 2.0, Bootstrap 5 e documentação interativa Swagger UI, com total equivalência ao projeto original em Spring Boot.

Próximo projeto da trilha → Versão 02:


🚀 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_jogos_01_fastapi

2. Execute a aplicação e os testes

pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
# testes automatizados: pytest -v

[!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_jogos_01_fastapi