Capítulo 54, Backend
FastAPI
O FastAPI transforma funções Python com anotações de tipo em uma API validada e documentada. Ele só faz sentido depois que você domina tipos, exceções e testes, e é por isso que ele vem agora.
Código deste capítulo: backend/cap54_fastapi_basico.py
Instalar e rodar
O FastAPI roda sobre o Starlette e usa o Pydantic para validação. O uvicorn é o servidor. O httpx2 é necessário para o cliente de testes (o Starlette 1.x o prefere, e com o httpx antigo ele ainda funciona, mas avisa que está obsoleto):
uv add fastapi uvicorn
uv add --dev httpx2 pytest
Com um arquivo main.py que defina app, o servidor sobe assim, e --reload recarrega ao salvar (só em desenvolvimento):
uv run uvicorn main:app --reload
Em seguida, abra http://127.0.0.1:8000/docs: o FastAPI gera uma página interativa com todas as rotas, a partir do seu código.
Rotas, modelos e validação
Cada rota é uma função. O parâmetro de caminho vem da URL, o parâmetro de consulta vem de ?x=1, e o corpo é um modelo Pydantic. A validação acontece antes de a sua função rodar: se os dados são inválidos, ela nem é chamada.
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI(title="Biblioteca")
class LivroCriar(BaseModel):
titulo: str = Field(min_length=1, max_length=100)
paginas: int = Field(gt=0)
class Livro(LivroCriar):
id: int
banco: dict[int, Livro] = {}
@app.post("/livros", response_model=Livro, status_code=201)
def criar(dados: LivroCriar) -> Livro:
livro = Livro(id=len(banco) + 1, **dados.model_dump())
banco[livro.id] = livro
return livro
@app.get("/livros/{livro_id}", response_model=Livro)
def obter(livro_id: int) -> Livro:
if livro_id not in banco:
raise HTTPException(status_code=404, detail="livro não encontrado")
return banco[livro_id]
@app.get("/livros")
def listar(minimo_paginas: Annotated[int, Query(ge=0)] = 0) -> list[Livro]:
return [livro for livro in banco.values() if livro.paginas >= minimo_paginas]
O response_model filtra a saída: se o seu objeto interno tiver um campo senha, ele não vaza, porque só o que está no modelo é serializado.
Testar sem subir servidor
O TestClient chama a aplicação diretamente, em memória, sem rede. É assim que eu testo toda rota:
cliente = TestClient(app)
print(cliente.post("/livros", json={"titulo": "Python na Prática", "paginas": 500}).json())
print(cliente.get("/livros/1").status_code)
nao_existe = cliente.get("/livros/9")
print(nao_existe.status_code, nao_existe.json())
{'titulo': 'Python na Prática', 'paginas': 500, 'id': 1}
200
404 {'detail': 'livro não encontrado'}
Quando o corpo é inválido, o FastAPI responde 422 com a lista exata dos campos errados, o local de cada um e o tipo do erro:
resposta = cliente.post("/livros", json={"titulo": "", "paginas": -1})
print(resposta.status_code)
print([(erro["loc"], erro["type"]) for erro in resposta.json()["detail"]])
422
[(['body', 'titulo'], 'string_too_short'), (['body', 'paginas'], 'greater_than')]
Injeção de dependências
Com Depends, uma rota declara do que precisa (uma sessão de banco, o usuário logado, uma configuração), e o FastAPI entrega. A vantagem decisiva é nos testes: dá para trocar a dependência real por uma falsa sem alterar a rota:
def obter_banco() -> dict[int, Livro]:
return banco
BancoDep = Annotated[dict[int, Livro], Depends(obter_banco)]
@app.get("/total")
def total(base: BancoDep) -> dict[str, int]:
return {"total": len(base)}
print(cliente.get("/total").json())
app.dependency_overrides[obter_banco] = lambda: {i: Livro(id=i, titulo="x", paginas=1) for i in range(5)}
print(cliente.get("/total").json())
app.dependency_overrides.clear()
print(sorted(app.openapi()["paths"]))
{'total': 1}
{'total': 5}
['/livros', '/livros/{livro_id}', '/total']
A última linha mostra que o contrato OpenAPI existe e lista as rotas. É essa especificação que alimenta a página /docs e que ferramentas geram clientes e testes de contrato.
def ou async def
Declare a rota com async def apenas se tudo que ela chama for assíncrono (await). Se ela chamar uma biblioteca síncrona (um driver de banco comum, o requests), use def simples: o FastAPI a executa em uma thread separada e o servidor continua responsivo. Uma rota async def que chama código bloqueante trava todas as requisições, o erro que o capítulo 47 já mostrou.
Onde cada responsabilidade mora
Nas rotas ficam só a tradução HTTP (ler a entrada, escolher o status, devolver a saída). A regra de negócio mora em um serviço, e o acesso ao banco em um repositório. É a separação que o capítulo de arquitetura apresentou, e o projeto final (capítulo 62) a aplica.
Exercício 1
Remover um livro
Acrescente DELETE /livros/{livro_id} que responda 204 quando remover e 404 quando o livro não existir.