APIs: REST e RESTful
O que REST significa de verdade, como escrever uma API em DataForge, e onde as decisões doem.
"API REST" virou sinônimo de "JSON sobre HTTP", e não é a mesma coisa. A diferença importa porque as regras de REST existem para resolver problemas concretos — cache, repetição, evolução — e ignorá-las custa exatamente esses três.
Os seis princípios, e o que cada um compra#
| Princípio | O que ele compra |
|---|---|
| cliente-servidor | as duas metades evoluem separadas |
| sem estado | qualquer servidor atende qualquer pedido — é o que permite escalar |
| cacheável | a resposta diz se pode ser guardada, e por quanto tempo |
| interface uniforme | recursos, verbos e representações iguais em todo lugar |
| em camadas | proxy, cache e balanceador entram sem o cliente saber |
| código sob demanda (opcional) | raramente usado |
Recurso é substantivo; o verbo é do HTTP#
GET /pedidos lista
POST /pedidos cria
GET /pedidos/42 lê um
PUT /pedidos/42 substitui inteiro
PATCH /pedidos/42 muda parte
DELETE /pedidos/42 apaga
GET /pedidos/42/itens o que pertence a eleO erro clássico é pôr o verbo no caminho — /criarPedido, /pedidoDelete. Quando isso acontece, cada endpoint vira um caso particular, e nada do que HTTP já sabe fazer (cache, repetição segura, código de status) se aplica sozinho.
Uma API em DataForge#
adopt Arcane.Database as DB
db := DB.memory()
DB.create_table(db, "pedidos", {"id": "INTEGER PRIMARY KEY",
"cliente": "TEXT", "total": "REAL"})
server api at "0.0.0.0" on 8000:
route GET "/pedidos":
pagina := int(query["pagina"] ?? "1")
respond DB.paginate(db, "pedidos", pagina, 20)
route GET "/pedidos/:id":
pedido := DB.query_one(db, "SELECT * FROM pedidos WHERE id = ?",
[params["id"]])
given pedido is void:
respond 404 json {"erro": "pedido nao encontrado"}
respond pedido
route POST "/pedidos":
cliente := body["cliente"] ?? ""
given cliente is "":
respond 400 json {"erro": "cliente e obrigatorio"}
id := DB.insert(db, "pedidos", {"cliente": cliente,
"total": body["total"] ?? 0.0})
respond 201 json {"id": id}
route DELETE "/pedidos/:id":
DB.delete(db, "pedidos", {"id": params["id"]})
respond 204 json {}
ignite apiO código de status é parte da resposta#
| Código | Quando |
|---|---|
| 200 | deu certo, e há corpo |
| 201 | criou — e o cabeçalho Location aponta o novo recurso |
| 204 | deu certo, e não há corpo (um DELETE) |
| 400 | o pedido está malformado |
| 401 | não sei quem é você |
| 403 | sei quem é você, e não pode |
| 404 | não existe |
| 409 | conflito — já existe, ou o estado mudou |
| 422 | entendi o formato, os valores é que não servem |
| 429 | devagar |
| 500 | o erro é meu |
Responder 200 com {"erro": …} dentro é o antipadrão mais comum: o cliente, o proxy e o monitoramento acham que deu certo, e só quem lê o corpo descobre que não.
Repetir sem estragar#
| Verbo | Repetir é seguro? | Muda dado? |
|---|---|---|
GET | sim | não |
PUT | sim — o resultado final é o mesmo | sim |
DELETE | sim — apagar duas vezes é apagar | sim |
POST | não | sim |
É por isso que um cliente que repete pedidos automaticamente não repete POST — repetir uma cobrança cobra duas vezes. Quando a repetição é necessária, a saída é a chave de idempotência:
route POST "/pedidos":
chave := headers["Idempotency-Key"] ?? ""
given chave isnt "":
ja := DB.query_one(db, "SELECT * FROM pedidos WHERE chave = ?", [chave])
given ja isnt void:
respond 200 json ja
// … cria, guardando a chave juntoQuem honra a chave é o servidor: o cliente só consegue oferecer o meio de reconhecer a repetição.
Erros com forma#
Um erro de API precisa ser processável por máquina e legível por pessoa. Um texto solto não é nem um nem outro:
action erro(codigo, mensagem, campos := {}):
yield {"erro": {"codigo": codigo,
"mensagem": mensagem,
"campos": campos}}
route POST "/pedidos":
problemas := {}
given (body["cliente"] ?? "") is "":
problemas["cliente"] := "obrigatorio"
given (body["total"] ?? 0.0) <= 0.0:
problemas["total"] := "precisa ser maior que zero"
given len(keys(problemas)) > 0:
respond 422 json erro("validacao", "confira os campos", problemas)O codigo é o que o cliente compara; a mensagem é o que a pessoa lê; os campos são o que o formulário marca em vermelho. Os três têm públicos diferentes.
Paginar#
// por página: simples, e caro nas páginas altas
respond DB.paginate(db, "pedidos", pagina, 20)
// por cursor: cada página custa o mesmo
respond DB.query(db,
"SELECT * FROM pedidos WHERE id > ? ORDER BY id LIMIT 20",
[int(query["depois_de"] ?? "0")])LIMIT 20 OFFSET 100000 parece constante e não é: o banco produz e descarta as cem mil primeiras linhas. Ver complexidade em dados.
Versionar#
| Forma | Prós e contras |
|---|---|
/v1/pedidos | visível e simples; duplica rota na virada |
cabeçalho Accept | "mais REST"; difícil de testar no navegador |
| não versionar, só acrescentar | o melhor, quando dá: campo novo não quebra ninguém |
A terceira linha é a que mais vale tentar primeiro. Um cliente bem escrito ignora campo que não conhece; quebrar só é inevitável quando algo sai ou muda de significado.
Segurança, no mínimo#
- Autenticação — JWT com
Arcane.Crypto, ou sessão. O middleware falha fechado: se ele erra, o handler não roda. - Autorização é outra coisa — saber quem é (401) não é poder fazer (403).
- Limite de taxa — sem ele, um cliente com laço derruba o serviço.
- Validar tudo o que vem de fora — corpo, query, cabeçalho e caminho.
- Nunca o erro interno no corpo — a mensagem do banco conta a estrutura da tabela para quem perguntar.
adopt Kiln
adopt Arcane.Crypto as Cr
steady SEGREDO := "troque-isto"
// O middleware recebe o pedido. Devolver uma resposta ENCERRA a cadeia;
// devolver 'void' deixa seguir, e o que ele guardou em req["state"]
// chega na rota.
action exigir_token(token):
dados := Cr.jwt_verificar(token, SEGREDO)
yield dados["carga"]["usuario"] given dados["valido"] otherwise void
server api on 0:
middleware Kiln.rate_limit(60, 60)
middleware Kiln.auth(exigir_token)
route GET "/eu":
respond json {"usuario": req["state"]["user"]}Documentar, e testar#
adopt Arcane.API as API
API.openapi(api, {"titulo": "API de Pedidos", "versao": "1.0.0"})Isso publica /openapi.json, que Insomnia, Postman e gerador de cliente leem. E o teste de rota não precisa de socket:
adopt Arcane.Crucible as C
adopt Arcane.Kiln as Kiln
crucible "api":
trial "GET /pedidos/999 devolve 404":
r := Kiln.test(api, "GET", "/pedidos/999")
expect r["status"] is 404
trial "POST sem cliente devolve 422":
r := Kiln.test(api, "POST", "/pedidos", {"total": 10.0})
expect r["status"] is 422
C.run()