Aula 4: FastAPI

Front-end e back-end

Até agora vimos como fazer páginas web com HTML, estilizá-las com CSS e torná-las dinâmicas com JavaScript. Tudo que escrevemos estava rodando em browsers, no que chamamos de lado do cliente ou front-end. Logicamente, não podemos ter todo o código das nossas aplicações web rodando no lado do cliente. Deve haver um componente remoto do nosso sistema web que funciona como uma fonte de verdade, recebe e processa dados dos clientes e envia respostas para eles. Esse componente é conhecido como back-end.

Diagrama mostrando a divisão entre front-end e back-end
Fonte: https://www.getwidget.dev/blog/connect-frontend-to-backend/

A interação com um sistema web (ou aplicativo web) simples funciona da seguinte forma:

Métodos HTTP

Os principais métodos HTTP usados em requisições são:

Além disso, cada interação com o servidor se dá através de uma rota com um método HTTP escolhido:

Documentação swagger mostrando rotas de um web app de uma pet store
Fonte: https://petstore.swagger.io/

Respostas HTTP

As respostas podem ter vários tipos de conteúdo e vários status que indicam se a requisição foi concluída com sucesso. O back-end pode, por exemplo:

Alguns exemplos de status de respostas HTTP são:

Código de Status Significado
200 OK
404 Not Found
500 Internal Server Error
403 Forbidden
301 Moved Permanently

Ambiente Virtual de Python

Ao trabalhar com Python, é uma boa prática usar ambientes virtuais para isolar os pacotes instalados para cada projeto. Dentro da pasta do nosso projeto podemos rodar o comando abaixo para criar um ambiente virtual:

    python -m venv .venv
    
Nota: existem ferramentas para gerenciar diferentes versões/instalações de Python em uma mesma máquina. Caso seja necessário para rodar diferentes projetos, é bom conhecer uma delas, como o pyenv.

Podemos então ativar o ambiente virtual:


    source .venv/bin/activate # para linux
    

    .venv\Scripts\Activate.ps1 # para windows
    

Após ativar o ambiente virtual, podemos instalar as dependências do nosso projeto. Uma forma básica de organizá-las é através de um arquivo requirements.txt


    touch requirements.txt                     # cria o arquivo com dependências
    echo fastapi[standard] > requirements.txt  # adiciona o pacote do fastapi no arquivo
    pip install -r requirements.txt            # instala dependências listadas no arquivo
    

Rodando o FastAPI

Podemos criar um back-end rapidamente dentro de um arquivo main.py:


    from fastapi import FastAPI

    app = FastAPI()


    @app.get("/")
    async def root():
        return {"message": "Hello World"}
    

O arquivo acima declara um back-end extremamente simples. Ele tem apenas uma rota (ou URL), acessável pelo método GET. O código da rota (ou endpoint) retorna um dicionário que é transformado pelo FastAPI em um JSON, que pode ser lido por código JavaScript no browser.


    fastapi dev # rodar servidor de aplicação em modo de desenvolvimento
    

O comando fastapi dev sobe um servidor de desenvolvimento na nossa máquina. Podemos acessar o nosso back-end a partir dele no endereço http://127.0.0.1:8000, também chamado de localhost. Diferentes aplicações rodando localmente podem usar diferentes portas no localhost, normalmente frameworks web usam a porta 8000.

Enquanto o servidor está rodando, podemos acessar localhost:8000/docs para ver a documentação das rotas do nosso web app, também chamada de API. Note que apesar do termo API ser comumente associado ao retorno de JSON, as rotas da API também podem retornar páginas na forma de HTML.

Interagindo com a API

Considere o código abaixo:

    from fastapi import FastAPI

    app = FastAPI()

    usuarios = ['Yuri', 'Rodrigo', 'Hugo']

    @app.get("/users/{index}")
    async def users(index):
        return {"nome": usuarios[index]}
    

Temos agora uma API que pode consultar usuários guardados no servidor de aplicação. Para consultar um usuário, abrimos o navegador e entramos em localhost:8000/users/{index}, sendo index um número de 0 a 2 (para acessar a lista corretamente).

A parte {index} da rota é o que chamamos de path parameter, é um parâmetro de busca inserido no path (ou rota). Ao fazer um request nesse endereço, o index é lido e passado como parâmetro para a função da rota. No entanto, o código acima não funciona porque o tipo padrão do index é string, o que não permite a indexação correta da lista.

Para resolver esse problema adicionamos um type hint, uma funcionalidade da linguagem Python que indica tipos de variáveis:


    from fastapi import FastAPI

    app = FastAPI()

    usuarios = ['Yuri', 'Rodrigo', 'Hugo']

    @app.get("/users/{index}")
    async def users(index: int):
        return {"nome": usuarios[index]}
    

Agora o FastAPI sabe como interpretar a variável index e faz o cast automatico para o seu tipo. Com isso, podemos acessar localhost:8000/users/1 e ler {"nome":"Rodrigo"}

Note que se acessarmos localhost:8000/users/string vamos ver um erro apropriado indicando que o tipo do nosso parâmetro de busca não é o que a API espera. Essa funcionalidade é muito útil para corrigir problemas em código que vai interagir com nosso back-end.

Tipagem em Python

Apesar de Python ser uma linguagem dinamicamente tipada podemos usar type hints, indicações no código do tipo que uma variável deve ter. Elas tornam o código mais compreensível e melhoram a interação com seu editor de código.


    def get_full_name(first_name, last_name):
        full_name = first_name.title() + " " + last_name.title()
        return full_name


    print(get_full_name("john", "doe"))
    

    def get_full_name(first_name: str, last_name: str):
        full_name = first_name.title() + " " + last_name.title()
        return full_name


    print(get_full_name("john", "doe"))
    

Os tipos básicos usados em anotação incluem:

Importando o módulo typing conseguimos usar outros tipos em anotações, como o Any


    from typing import Any
    
    def some_function(data: Any):
        print(data)
    

Podemos também usar tipos genéricos, que aceitam outros tipos dentro:


    def process_items_list(items: list[str]):
        for item in items:
            print(item.capitalize())

    def process_items_tuple(items_t: tuple[int, int, str], items_s: set[bytes]):
        return items_t, items_s

    def process_items_dict(prices: dict[str, float]):
        for item_name, item_price in prices.items():
            print(item_name)
            print(item_price)
    

Caso uma variável possa ter mais de um tipo podemos declarar uma união de tipos:


    def process_item(item: int | str):
        print(item)

    def say_hi(name: str | None = None): # Aqui indicamos que name pode ser None
        if name is not None:
            print(f"Hey {name}!")
        else:
            print("Hello World")
    

Por fim, podemos declarar uma classe como o tipo de uma variável. Isso é especialmente útil quando programamos com FastAPI para fazer a verificação dos dados enviados para um endpoint.


    class Person:
        def __init__(self, name: str):
            self.name = name

    def get_person_name(one_person: Person):
        return one_person.name
    

Geralmente o comportamento do código não muda quando adicionamos type hints. O FastAPI, entretanto, é baseado na biblioteca Pydantic, que implementa uma série de comportamentos adicionais baseados nas anotações de tipos. Com o Pydantic declaramos classes que serão instanciadas com valores externos que serão validados e convertidos automaticamente, como no exemplo abaixo:


    from datetime import datetime

    from pydantic import BaseModel


    class User(BaseModel):
        id: int
        name: str = "John Doe"
        signup_ts: datetime | None = None
        friends: list[int] = []


    external_data = {
        "id": "123",
        "signup_ts": "2017-06-01 12:22",
        "friends": [1, "2", b"3"],
    }
    user = User(**external_data)
    print(user)
    # > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
    print(user.id)
    # > 123
    

Query parameters

Quando declaramos parâmetros na função da rota que não fazem parte do path, eles automaticamente são interpretados como query parameters, ou parâmetros de consulta.


    from fastapi import FastAPI

    app = FastAPI()

    fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


    @app.get("/items/")
    async def read_item(skip: int = 0, limit: int = 10):
        return fake_items_db[skip : skip + limit]
    

No endereço http://127.0.0.1:8000/items/?skip=0&limit=10 os parâmetros são skip com valor 0 e limit com valor 10. Eles naturalmente são strings, pela forma como o protocolo HTTP funciona, mas o FastAPI também usa as type hints para fazer a verificação e conversão automática de tipos dos parâmetros.

Note que podemos tornar o parâmetro opcional ao declarar um valor padrão para ele. Também podemos indicar que seu valor pode ser None usando type hints. Caso não seja declarado um valor padrão para o parâmetro, ele se torna obrigatório.


    from fastapi import FastAPI

    app = FastAPI()


    @app.get("/items/{item_id}")
    async def read_user_item(item_id: str, needy: str):
        item = {"item_id": item_id, "needy": needy}
        return item
    

Corpo do request

Quando mandamos dados para a nossa aplicação através da API, por exemplo com um request POST, eles são enviados no corpo do request. Com FastAPI fazemos a verificação dos dados recebidos nesses requests através de classes do Pydantic:


    from fastapi import FastAPI
    from pydantic import BaseModel


    class Item(BaseModel):               # aqui declaramos os dados que queremos receber e seus tipos
        name: str                        # campo necessário
        description: str | None = None   # campo opcional, valor padrão None
        price: float
        tax: float | None = None


    app = FastAPI()


    @app.post("/items/")
    async def create_item(item: Item):  # adicionamos a type hynt com a classe que criamos para instanciar um item
        return item
    

Automaticamente o FastAPI fará

Renderizando HTML

Podemos enviar HTML no corpo da resposta da API para que o browser renderize algo na página. Com FastAPI fazemos isso através de uma HTMLResponse:


    from fastapi import FastAPI
    from fastapi.responses import HTMLResponse

    app = FastAPI()


    @app.get("/items/", response_class=HTMLResponse)
    async def read_items():
        return """
        <html>
            <head>
                <title>Some HTML in here</title>
            </head>
            <body>
                <h1>Look ma! HTML!</h1>
            </body>
        </html>
        """
    

Concorrência e async/await

Talvez você tenha estranhado a linha de código abaixo:


    async def root():
    

A declaração de função aqui usa o async, uma sintaxe de Python que organiza a execução de código assíncrono junto com o await. Em linhas gerais, usamos o async ao declarar funções que usem o await dentro, e usamos o await quando uma biblioteca diz que deveríamos usar. Geralmente o uso do await é associado à comunicação com componentes externos ao código ou processos demorados.


    @app.get('/')
    async def read_results():
        results = await some_library()
        return results
    

Declarar funções com o async permite que o FastAPI otimize o servidor de aplicação para que ele possa ser usado por mais usuários ao mesmo tempo e que os tempos de resposta sejam os mais rápidos possíveis.

Exercícios

Exercício de Sala

Faça uma API com os cinco endpoints:

Abaixo está a página que vai ser enviada através da API e vai renderizar as respostas dos outros requests:


    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
        <script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/json-enc.js"></script>
        <title>Requests</title>
        <style>
            body {
                display: flex;
                gap: 2.5vw;
                justify-content: center;
                min-height: 90vh;
                background-color: #292827;
                color: #e0e0e0;
            }

            .secao-interacao, .secao-respostas {
                border: 2px solid #ff690a;
                border-radius: 15px;
                padding: 20px;
                width: 50%;
                height: auto;
            }

            .secao-interacao, form {
                display: flex;
                flex-direction: column;
            }

            @media(orientation: portrait) {
                body {
                    flex-direction: column;
                    gap: 2.5vh;
                    min-height: auto;
                }

                .secao-interacao, .secao-respostas {
                    min-height: 30vh;
                    width: auto;
                }
            }

            #json-insert {
                color: #ff690a;
                font-size: xx-large;
            }

            /* Estilização dos elementos de interação com inputs */
            label {
                margin-top: 15px;
                margin-bottom: 5px;
                font-weight: bold;
                font-size: 0.9rem;
                color: #ff690a;
            }

            input[type="text"], 
            input[type="number"] {
                background-color: #1e1e1e;
                border: 1px solid #444;
                border-radius: 8px;
                padding: 12px 15px;
                color: #e0e0e0;
                font-size: 1rem;
                transition: all 0.3s ease;
                outline: none;
            }

            input[type="text"]:hover, 
            input[type="number"]:hover {
                border-color: #666;
            }

            input[type="text"]:focus, 
            input[type="number"]:focus {
                border-color: #ff690a;
                box-shadow: 0 0 8px rgba(255, 105, 10, 0.3);
                background-color: #252525;
            }

            input[type="submit"], button {
                margin-top: 20px;
                padding: 12px;
                border-radius: 8px;
                border: none;
                background-color: #ff690a;
                color: #fff;
                font-weight: bold;
                cursor: pointer;
                transition: transform 0.1s, background-color 0.2s;
            }

            input[type="submit"]:hover, button:hover {
                background-color: #e55a00;
            }

            input[type="submit"]:active, button:active {
                transform: scale(0.98);
            }

            hr {
                border: 0;
                border-top: 1px solid #444;
                margin: 25px 0;
                width: 100%;
            }
        </style>
    </head>
    <body>
        <div class="secao-interacao">
            <h1>Requests</h1>
            <form hx-post="/users"
                hx-trigger="submit"
                hx-target="#json-insert"
                hx-swap="innerHTML"
                hx-ext="json-enc">  
                <label for="nome">Nome do usuário</label>
                <input type="text" name="nome">
                <label for="idade">Idade do usuário</label>
                <input type="number" name="idade">
                <input type="submit">
            </form>
            <hr>
            <input type="number"
                name="index"
                hx-get="/users"
                hx-trigger="input changed"
                hx-target="#json-insert"
                hx-swap="innerHTML"
                placeholder="Índice do usuário">
            <hr>
            <button hx-get="/users"
                    hx-target="#json-insert"
                    hx-swap="innerHTML">
                Obter todos os usuários
            </button>
            <hr>
            <button hx-delete="/users"
                    hx-target="#json-insert"
                    hx-swap="innerHTML">
                Apagar todos os usuários
            </button>
        </div>

        <div class="secao-respostas">
            <h1>Respostas</h1>
            <div id="json-insert"></div>
        </div>
    </body>
    </html>