Pular para o conteúdo

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ípioO que ele compra
cliente-servidoras duas metades evoluem separadas
sem estadoqualquer servidor atende qualquer pedido — é o que permite escalar
cacheávela resposta diz se pode ser guardada, e por quanto tempo
interface uniformerecursos, verbos e representações iguais em todo lugar
em camadasproxy, cache e balanceador entram sem o cliente saber
código sob demanda (opcional)raramente usado

Recurso é substantivo; o verbo é do HTTP#

text
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 ele

O 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#

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 api

O código de status é parte da resposta#

CódigoQuando
200deu certo, e há corpo
201criou — e o cabeçalho Location aponta o novo recurso
204deu certo, e não há corpo (um DELETE)
400o pedido está malformado
401não sei quem é você
403sei quem é você, e não pode
404não existe
409conflito — já existe, ou o estado mudou
422entendi o formato, os valores é que não servem
429devagar
500o 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#

VerboRepetir é seguro?Muda dado?
GETsimnão
PUTsim — o resultado final é o mesmosim
DELETEsim — apagar duas vezes é apagarsim
POSTnãosim

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

dataforge
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 junto

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

dataforge
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#

dataforge
// 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#

FormaPrós e contras
/v1/pedidosvisível e simples; duplica rota na virada
cabeçalho Accept"mais REST"; difícil de testar no navegador
não versionar, só acrescentaro 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.
dataforge
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#

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

dataforge
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()

Por onde seguir#