Pular para o conteúdo

    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):

    Terminal
    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):

    Terminal
    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.

    backend/cap54_fastapi_basico.pylinhas 10 a 47
    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:

    backend/cap54_fastapi_basico.pylinhas 52 a 56
    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())
    
    Saída
    {'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:

    backend/cap54_fastapi_basico.pylinhas 58 a 60
    resposta = cliente.post("/livros", json={"titulo": "", "paginas": -1})
    print(resposta.status_code)
    print([(erro["loc"], erro["type"]) for erro in resposta.json()["detail"]])
    
    Saída
    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:

    backend/cap54_fastapi_basico.pylinhas 65 a 82
    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"]))
    
    Saída
    {'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.