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.
A interação com um sistema web (ou aplicativo web) simples funciona da seguinte forma:
Os principais métodos HTTP usados em requisições são:
GET lê algum recurso da aplicaçãoPOST publica algum recurso na aplicaçãoPUT atualiza algum recurso da aplicaçãoDELETE apaga algum recurso da aplicaçãoAlém disso, cada interação com o servidor se dá através de uma rota com um método HTTP escolhido:
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 |
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
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.
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.
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:
intfloatboolbytes
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:
listtuplesetdict
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
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
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á
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>
"""
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.
Faça uma API com os cinco endpoints:
@app.get('/') Envia a página HTML abaixo para o usuário@app.post("/users/") Adiciona um usuário numa lista@app.get("/users/") Lê todos os usuários da lista ou um índice específico (usando query parameter)@app.delete("/users") Limpa a lista de usuáriosAbaixo 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>