Pular para o conteúdo

    Capítulo 8, Básico

    CRUD completo: uma lista de tarefas

    Criar, ler, atualizar e remover é o esqueleto de quase toda API. Aqui eu monto um CRUD completo guardado em memória, e acerto os pontos em que o exemplo clássico dá respostas erradas.

    O desenho

    Um CRUD precisa de quatro rotas sobre o mesmo recurso, e eu as organizo em torno de três modelos: o que o cliente envia para criar (sem id), o que a API devolve (com id), e o que o cliente envia para uma alteração parcial (todos os campos opcionais). O id é gerado pelo servidor: deixar o cliente escolher o próprio id abre espaço para colisões.

    basico/cap08_crud_tarefas.pylinhas 10 a 34
    from itertools import count
    
    from fastapi import FastAPI, HTTPException, status
    from fastapi.testclient import TestClient
    from pydantic import BaseModel, Field
    
    app = FastAPI()
    
    
    class TarefaEntrada(BaseModel):
        titulo: str = Field(min_length=1, max_length=100)
        concluida: bool = False
    
    
    class Tarefa(TarefaEntrada):
        id: int
    
    
    class TarefaParcial(BaseModel):
        titulo: str | None = Field(default=None, min_length=1, max_length=100)
        concluida: bool | None = None
    
    
    tarefas: dict[int, Tarefa] = {}
    proximo_id = count(1)
    

    Uma lista funcionaria, mas um dicionário indexado pelo id encontra uma tarefa direto, sem percorrer tudo. O banco de dados real (capítulo 16) vai ocupar o lugar dele.

    Criar e ler

    O POST devolve 201 (criado), e o GET aceita um filtro opcional. A busca por id fica em uma função só, que levanta o 404 quando não acha:

    basico/cap08_crud_tarefas.pylinhas 39 a 63
    def buscar_ou_404(tarefa_id: int) -> Tarefa:
        tarefa = tarefas.get(tarefa_id)
        if tarefa is None:
            raise HTTPException(status.HTTP_404_NOT_FOUND, "Tarefa não encontrada")
        return tarefa
    
    
    @app.post("/tarefas", response_model=Tarefa, status_code=status.HTTP_201_CREATED)
    def criar(entrada: TarefaEntrada):
        tarefa = Tarefa(id=next(proximo_id), **entrada.model_dump())
        tarefas[tarefa.id] = tarefa
        return tarefa
    
    
    @app.get("/tarefas", response_model=list[Tarefa])
    def listar(concluida: bool | None = None):
        itens = list(tarefas.values())
        if concluida is not None:
            itens = [t for t in itens if t.concluida == concluida]
        return itens
    
    
    @app.get("/tarefas/{tarefa_id}", response_model=Tarefa)
    def obter(tarefa_id: int):
        return buscar_ou_404(tarefa_id)
    

    Atualizar: PUT substitui, PATCH altera

    O PUT substitui o recurso inteiro: o cliente manda todos os campos. O PATCH altera só o que veio, e o model_dump(exclude_unset=True) do capítulo anterior é o que torna isso possível, porque separa "o cliente mandou false" de "o cliente não mandou nada":

    basico/cap08_crud_tarefas.pylinhas 68 a 85
    @app.put("/tarefas/{tarefa_id}", response_model=Tarefa)
    def substituir(tarefa_id: int, entrada: TarefaEntrada):
        buscar_ou_404(tarefa_id)
        tarefas[tarefa_id] = Tarefa(id=tarefa_id, **entrada.model_dump())
        return tarefas[tarefa_id]
    
    
    @app.patch("/tarefas/{tarefa_id}", response_model=Tarefa)
    def alterar(tarefa_id: int, mudancas: TarefaParcial):
        atual = buscar_ou_404(tarefa_id)
        tarefas[tarefa_id] = atual.model_copy(update=mudancas.model_dump(exclude_unset=True))
        return tarefas[tarefa_id]
    
    
    @app.delete("/tarefas/{tarefa_id}", status_code=status.HTTP_204_NO_CONTENT)
    def remover(tarefa_id: int):
        buscar_ou_404(tarefa_id)
        del tarefas[tarefa_id]
    

    O DELETE devolve 204 sem corpo: deu certo, e não há nada a mostrar.

    O fluxo completo

    basico/cap08_crud_tarefas.pylinhas 90 a 97
    cliente = TestClient(app)
    
    a = cliente.post("/tarefas", json={"titulo": "Estudar FastAPI"})
    b = cliente.post("/tarefas", json={"titulo": "Escrever testes", "concluida": True})
    print(a.status_code, a.json())
    print(b.json())
    print([t["titulo"] for t in cliente.get("/tarefas").json()])
    print([t["titulo"] for t in cliente.get("/tarefas?concluida=true").json()])
    
    Saída
    201 {'titulo': 'Estudar FastAPI', 'concluida': False, 'id': 1}
    {'titulo': 'Escrever testes', 'concluida': True, 'id': 2}
    ['Estudar FastAPI', 'Escrever testes']
    ['Escrever testes']
    
    basico/cap08_crud_tarefas.pylinhas 99 a 103
    print(cliente.put("/tarefas/1", json={"titulo": "Estudar FastAPI com calma"}).json())
    print(cliente.patch("/tarefas/1", json={"concluida": True}).json())
    print(cliente.delete("/tarefas/2").status_code)
    print(cliente.get("/tarefas/2").status_code, cliente.get("/tarefas/2").json())
    print(len(cliente.get("/tarefas").json()))
    
    Saída
    {'titulo': 'Estudar FastAPI com calma', 'concluida': False, 'id': 1}
    {'titulo': 'Estudar FastAPI com calma', 'concluida': True, 'id': 1}
    204
    404 {'detail': 'Tarefa não encontrada'}
    1
    

    Repare no PATCH: ele mudou só concluida, e o título que o PUT tinha acabado de definir ficou intacto.

    O que eu mudei em relação ao exemplo clássico

    Exemplo clássicoAquiPor quê
    O cliente envia o idO servidor geraO cliente pode repetir um id existente
    return {"error": "não encontrado"}raise HTTPException(404)O primeiro devolve 200 com uma mensagem de erro, e o cliente não percebe que falhou
    POST devolve 200201 CreatedO status diz o que aconteceu
    DELETE devolve uma mensagem204 No ContentNão há corpo a devolver
    Uma lista percorrida com forDicionário por idA busca é direta
    Um modelo para tudoEntrada, saída e parcialCada um tem campos diferentes

    Memória não é banco de dados

    Estes dados somem quando o servidor reinicia, e cada processo tem a sua cópia: com dois processos (--workers 2), uma tarefa criada em um pode não existir no outro. Use isso só para aprender. O capítulo 16 troca o dicionário por um banco de verdade, sem mudar as rotas.

    Exercício 1

    Estatísticas das tarefas

    Acrescente GET /estatisticas, que devolva {"total": ..., "concluidas": ..., "pendentes": ...}. Teste com tarefas novas, em um app limpo.