404 Not Found

404 Not Found


nginx

Exercício Abrangente da Fase 1 — Construindo a API Básica do PriceTracker

Os conceitos abordados nas cinco aulas da Fase 1 são como cinco peças de quebra-cabeça. Agora é hora de juntá-las para formar uma imagem completa—a API Básica do PriceTracker—para que o front end do Bob possa realmente consumir os dados.

1. O Que Você Vai Aprender


2. A História Real da Alice

(1) Problema: Conhecimento fragmentado não pode ser montado em um produto

Alice terminou as primeiras cinco aulas, mas os exemplos em cada aula são trechos independentes—os exemplos sobre parâmetros de path e Pydantic não estão conectados. Bob não podia esperar mais: "Eu preciso de uma API funcional, não de um monte de demos espalhadas!" Alice precisa integrar design de roteamento, validação de parâmetros, validação de dados e filtragem de resposta em um único serviço de API funcional.

(2) Soluções Abrangentes para Problemas do Mundo Real

Esta aula integra todos os conceitos abordados nas primeiras cinco aulas na API Básica do PriceTracker, que inclui um conjunto completo de endpoints CRUD, um sistema de modelo Pydantic, uma cadeia de validação de parâmetros e lógica de filtragem de resposta.

PYTHON
# Estrutura básica completa da API do PriceTracker
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="PriceTracker API", version="0.1.0")

# Modelos, rotas, validação — tudo integrado

(3) Resultado

Alice tem um serviço de API funcional, e Bob pode usar o Swagger UI para testar todos os endpoints; a documentação OpenAPI gerada automaticamente permite que SDKs front-end consumam a API diretamente.


3. Design Abrangente dos Endpoints da API

(1) Planejamento dos Endpoints da Fase 1

100%
flowchart LR
    Client[Client] -->|GET /products| List[List Products]
    Client -->|POST /products| Create[Create Product]
    Client -->|GET /products/id| Detail[Product Detail]
    Client -->|PUT /products/id| Update[Update Product]
    Client -->|DELETE /products/id| Delete[Delete Product]
    Client -->|GET /prices| Search[Search Prices]
    Client -->|POST /prices| AddPrice[Add Price]
    
    List --> PM[ProductResponse]
    Create --> PM
    Detail --> PDP[ProductDetailResponse]
    Search --> PRM[PriceResponse]
    AddPrice --> PRM
Endpoint Método Parâmetros de Path Parâmetros de Query Corpo da Requisição Modelo de Resposta
Lista de Produtos GET - category, sort, limit, offset - list[ProductResponse]
Criar Produto POST - - ProductCreate ProductResponse
Detalhes do Produto GET product_id - - ProductDetailResponse
Atualizar Produto PUT product_id - ProductUpdate ProductResponse
Deletar Produto DELETE product_id - - dict
Consulta de Preço GET - product_id, min_price, max_price - list[PriceResponse]
Adicionar Preço POST - - PriceCreate PriceResponse

(1) ▶ Exemplo: Sistema de Modelo Pydantic Completo

PYTHON
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import Optional
from enum import Enum
from datetime import datetime

class Category(str, Enum):
    electronics = "electronics"
    clothing = "clothing"
    food = "food"
    books = "books"

class PriceInfo(BaseModel):
    amount: float = Field(gt=0, description="Valor do preço em USD")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    category: Category
    base_price: float = Field(gt=0, description="Preço base em USD")
    description: Optional[str] = Field(None, max_length=2000)

class ProductUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=200)
    category: Optional[Category] = None
    base_price: Optional[float] = Field(None, gt=0)
    description: Optional[str] = Field(None, max_length=2000)

class ProductResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    category: str
    base_price: float

class ProductDetailResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    category: str
    base_price: float
    description: Optional[str] = None
    created_at: datetime

class PriceCreate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0, description="Preço em USD")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
    source: str = Field(max_length=100)

    @field_validator("price")
    @classmethod
    def round_price(cls, v: float) -> float:
        return round(v, 2)

class PriceResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    product_id: int
    price: float
    currency: str
    source: str
    recorded_at: datetime

Saída:

TEXT
# Função definida com sucesso

4. Ciclo de Vida Requisição-Resposta

(1) Fluxo de Trabalho Completo

100%
sequenceDiagram
    participant Client as Bob Frontend
    participant FastAPI as FastAPI Router
    participant Pydantic as Pydantic Validator
    participant Handler as View Function
    participant DB as In-Memory DB

    Client->>FastAPI: POST /products (JSON body)
    FastAPI->>Pydantic: Validate with ProductCreate
    Pydantic-->>FastAPI: Instância do modelo validada
    FastAPI->>Handler: create_product(data: ProductCreate)
    Handler->>DB: Store product
    DB-->>Handler: Stored record
    Handler-->>FastAPI: Return full dict
    FastAPI->>Pydantic: Filter with ProductResponse
    Pydantic-->>Client: JSON response (filtrada)

(1) ▶ Exemplo: Implementando Endpoints CRUD de Produto

PYTHON
from fastapi import FastAPI, Path, Query, HTTPException
from datetime import datetime

app = FastAPI(title="PriceTracker API", version="0.1.0")

# Armazenamento em memória (será substituído por BD na Fase 2)
products_db: dict[int, dict] = {}
prices_db: list[dict] = []
_product_counter = 0
_price_counter = 0

@app.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(product: ProductCreate):
    global _product_counter
    _product_counter += 1
    record = {
        "id": _product_counter,
        "name": product.name,
        "category": product.category.value,
        "base_price": product.base_price,
        "description": product.description,
        "created_at": datetime.utcnow(),
    }
    products_db[_product_counter] = record
    return record

@app.get("/products", response_model=list[ProductResponse])
async def list_products(
    category: Optional[Category] = Query(None),
    sort: str = Query("name", pattern="^(name|base_price)$"),
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    items = list(products_db.values())
    if category:
        items = [p for p in items if p["category"] == category.value]
    items.sort(key=lambda p: p.get(sort, ""))
    return items[offset : offset + limit]

@app.get("/products/{product_id}", response_model=ProductDetailResponse)
async def get_product(
    product_id: int = Path(gt=0, description="Product ID"),
):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return products_db[product_id]

Saída:

TEXT
# Função definida com sucesso

(2) ▶ Exemplo: Endpoints de Atualização e Exclusão

PYTHON
@app.put("/products/{product_id}", response_model=ProductResponse)
async def update_product(
    product_id: int = Path(gt=0),
    update: ProductUpdate = ...,
):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    record = products_db[product_id]
    update_data = update.model_dump(exclude_unset=True)
    record.update(update_data)
    return record

@app.delete("/products/{product_id}")
async def delete_product(product_id: int = Path(gt=0)):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    del products_db[product_id]
    return {"message": "Product deleted"}

Saída:

TEXT
# Função definida com sucesso

5. Endpoints de Preço e Estratégias de Portfólio na Prática

(1) Combinação de Parâmetros de Query e Corpo da Requisição

(1) ▶ Exemplo: Endpoints de Consulta e Criação de Preço

PYTHON
@app.get("/prices", response_model=list[PriceResponse])
async def search_prices(
    product_id: Optional[int] = Query(None, gt=0),
    min_price: float = Query(0, ge=0, description="Preço mín em USD"),
    max_price: float = Query(999999, ge=0, description="Preço máx em USD"),
    limit: int = Query(50, ge=1, le=200),
    offset: int = Query(0, ge=0),
):
    results = prices_db
    if product_id:
        results = [p for p in results if p["product_id"] == product_id]
    results = [p for p in results if min_price <= p["price"] <= max_price]
    return results[offset : offset + limit]

@app.post("/prices", response_model=PriceResponse, status_code=201)
async def create_price(price: PriceCreate):
    global _price_counter
    if price.product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    _price_counter += 1
    record = {
        "id": _price_counter,
        "product_id": price.product_id,
        "price": price.price,
        "currency": price.currency,
        "source": price.source,
        "recorded_at": datetime.utcnow(),
    }
    prices_db.append(record)
    return record

Saída:

TEXT
# Função definida com sucesso

6. Guia Prático de Filtragem de Resposta: Versão Pública vs. Versão Admin

(1) Cenário de Negócios: Ocultando Preços de Atacado

Usuários de varejo do PriceTracker só podem visualizar preços de varejo, enquanto administradores podem visualizar preços de atacado e de custo.

(1) ▶ Exemplo: Modelo de Resposta Multi-Papel

PYTHON
class ProductPublicResponse(BaseModel):
    """Visualização pública - ocultar preços de atacado e custo"""
    id: int
    name: str
    category: str
    retail_price: float

class ProductAdminResponse(BaseModel):
    """Visualização admin - mostrar cadeia completa de preços"""
    id: int
    name: str
    category: str
    retail_price: float
    wholesale_price: float
    cost_price: float
    margin_pct: float  # Percentual de margem de lucro

# Armazenamento em memória estendido com níveis de preço
admin_products_db: dict[int, dict] = {}

@app.get("/products/{product_id}/public", response_model=ProductPublicResponse)
async def get_product_public(product_id: int = Path(gt=0)):
    if product_id not in admin_products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return admin_products_db[product_id]

@app.get("/products/{product_id}/admin", response_model=ProductAdminResponse)
async def get_product_admin(product_id: int = Path(gt=0)):
    if product_id not in admin_products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return admin_products_db[product_id]

Saída:

TEXT
# Função definida com sucesso

❓ Perguntas Frequentes

P O que devo fazer se dados forem perdidos do armazenamento em memória após uma reinicialização?
R A Fase 1 usa armazenamento em memória para simplificar o aprendizado; na Fase 2, será substituído por PostgreSQL + SQLAlchemy. Esta é uma estratégia de aprendizado progressiva.
P Por que ProductCreate e ProductResponse são separados?
R Separação de preocupações: O modelo Create não inclui um ID (que é gerado no servidor), enquanto o modelo Response inclui um ID. Esta também é uma melhor prática de segurança—o cliente não deve especificar o ID.
P Qual é o propósito de exclude_unset=True no endpoint PUT?
R Permite que o cliente atualize apenas os campos que deseja alterar; campos não fornecidos não são sobrescritos e permanecem definidos como None. Combinado com os campos opcionais em ProductUpdate, isso habilita atualizações parciais.
P Como posso testar se um documento OpenAPI pode ser consumido por uma aplicação front-end?
R Visite /openapi.json, use npx openapi-typescript para gerar tipos TypeScript, ou use Swagger Codegen para gerar um SDK.
P Como os valores de parâmetros de enumeração são exibidos na documentação?
R O FastAPI exibe automaticamente todos os valores de enum no menu suspenso do Swagger UI, tornando-os imediatamente claros para Bob, o desenvolvedor front-end.
P Como evitar duplicação quando múltiplos endpoints compartilham um modelo?
R Use herança de modelo: Defina campos comuns em ProductBase(BaseModel), e faça ProductCreate(ProductBase) e ProductResponse(ProductBase) herdarem e os estenderem.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade ⭐): Combine todo o código desta aula em um único app/main.py, inicie o serviço, e use o Swagger UI para criar três produtos e consultar a lista. Dica: uvicorn app.main:app --reload
  2. Exercício Avançado (Dificuldade ⭐⭐): Adicione o endpoint /products/{id}/prices para consultar todos os registros de preço de um produto especificado, suportando os parâmetros de query sort (date/price) e limit. Dica: parâmetro de path product_id + parâmetros de query sort/limit
  3. Desafio (Dificuldade: ⭐⭐⭐): Implemente um sistema de visualização dupla usando ProductAdmin e ProductPublic—crie dados de produto contendo a cadeia completa de preços. O endpoint público deve retornar apenas o preço de varejo, enquanto o endpoint administrativo deve retornar o preço de varejo, preço de atacado e margem de lucro. Dica: Duas instâncias de response_model + mesma fonte de dados

---|

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%