Pular para o conteúdo

Kiln — o framework web

Servidor HTTP, rotas, páginas e API REST, com sintaxe própria na linguagem e zero dependências.

Kiln é o framework web do DataForge. O nome vem do forno onde a peça ganha a forma final: a requisição entra crua e sai como resposta.

Ele não é um módulo como os outros. O Kiln tem sintaxe própria na linguagemserver, route, respond, render, redirect, middleware, mount, assets, views e ignite — para que uma rota se leia como uma rota, e não como uma chamada de função com um lambda dentro.

Um servidor inteiro#

dataforge
adopt Kiln

produtos := [
    {"id": 1, "nome": "Martelo", "preco": 89.9},
    {"id": 2, "nome": "Bigorna", "preco": 450.0}
]

server loja on 8080:
    middleware Kiln.logger()
    middleware Kiln.cors()

    route GET "/":
        respond html "<h1>Forja</h1><p>2 itens no catálogo</p>"

    route GET "/produtos":
        respond json {"itens": produtos, "total": len(produtos)}

    route GET "/produtos/:id":
        p := achar(int(params["id"]))
        given p is void:
            respond 404 json {"erro": "não achei"}
        respond json p

    route POST "/produtos":
        produtos.append(body)
        respond 201 json body

ignite loja

Isso é um servidor completo: HTML, JSON, parâmetro de caminho, corpo interpretado, status certo. Não há arquivo de configuração, nem decorador, nem registro manual de rota.

As onze palavras#

PalavraFaz
server nome on porta:declara a aplicação e liga ao nome
route VERBO "caminho":registra uma rota
respond [status] [tipo] valorenvia a resposta e encerra a rota
render "arquivo" with dadosrenderiza um template e encerra a rota
redirect "/destino"302 com Location (ou status 301)
middleware expressaoroda antes de toda rota
after expressaoroda depois, com a resposta na mão
mount outro at "/prefixo"junta outro server sob um prefixo
assets "/prefixo" from "pasta"serve arquivos do disco
views "pasta"onde ficam os templates
ignite nome [on porta]acende o forno: sobe e bloqueia

middleware corta o pedido antes da rota — autenticação, limite de taxa. after recebe a resposta pronta e pode trocá-la — cabeçalhos, compressão, cache. Todas são contextuais: só valem dentro de um bloco server. Fora dali, route, render e server continuam sendo nomes livres — render := 42 é uma variável perfeitamente válida, e nenhum programa escrito antes do Kiln parou de compilar por causa dele.

Declarar não é subir#

server monta a aplicação e liga ao nome. Quem acende o forno é ignite. A separação parece pedante até você escrever o primeiro teste:

dataforge
// executa a rota direto na aplicação, sem abrir socket
r := Kiln.test(loja, "GET", "/produtos/2")
out r["status"], r["body"]["nome"]    // 200 Bigorna

Testar uma rota fica tão barato quanto testar uma ação — que é o que faz alguém realmente escrever esses testes. O projeto loja-web tem 29 deles, e todos juntos rodam em 0,06 s.

O que vem de graça#

SituaçãoO Kiln faz
caminho não registrado404
caminho existe, verbo não405 com o cabeçalho Allow
OPTIONS com Kiln.cors()responde o preflight, sem chegar na rota
erro dentro da rota500, detalhe no terminal, servidor de pé
JSON quebrado no corpochega como texto — a rota decide se é 400
corpo grande demais413 antes de ler tudo na memória
../ num caminho estático403, antes de abrir o arquivo

A distinção entre 404 e 405 não é preciosismo: dizer "esse caminho existe, mas não com esse verbo" poupa quem consome a API de procurar um bug que não existe.

O que já vem pronto#

Estas não são bibliotecas para instalar: fazem parte do Kiln, e não têm dependência nenhuma.

dataforge
server api on 8080:
    middleware Kiln.request_id()             // um id por pedido
    middleware Kiln.limite_de_corpo(1048576) // 413 acima de 1 MB
    middleware Kiln.rate_limit(60, 60)       // 60 por minuto, por IP
    middleware Kiln.csrf(SEGREDO)            // recusa POST de fora
    middleware Kiln.validar(ESQUEMA)         // 422 com todos os campos
    middleware Kiln.idempotente()            // não cobra duas vezes

    after Kiln.cache(120)                    // ETag + 304
    after Kiln.cabecalhos_seguros()          // CSP, nosniff, frame
    after Kiln.comprimir()                   // gzip quando compensa
    after Kiln.auditoria()                   // quem mudou o quê

    route GET "/produtos":
        achados := Kiln.buscar(produtos, req, ["nome"])
        respond Kiln.paginar(Kiln.ordenar(achados, req, ["preco"]), req)

Listar bem é mais que devolver a lista#

GET /produtos?q=martelo&ordenar=-preco&pagina=2&por_pagina=10 — as três coisas que toda API precisa, e que quase sempre são reescritas à mão em cada rota:

json
{
  "itens": [ … ],
  "pagina": 2, "por_pagina": 10,
  "total": 45, "paginas": 5,
  "tem_proxima": yes, "tem_anterior": yes
}

Validação que relata tudo de uma vez#

dataforge
ESQUEMA := {
    "nome":  {"tipo": "texto", "obrigatorio": yes, "min": 3, "max": 40},
    "preco": {"tipo": "numero", "min": 0},
    "email": {"tipo": "email"},
    "papel": {"tipo": "texto", "em": ["admin", "leitor"]},
}
422
{
  "erro": "dados inválidos",
  "campos": {
    "nome": "mínimo 3",
    "preco": "mínimo 0",
    "papel": "valor fora da lista permitida"
  }
}

Um erro por envio faz quem preenche descobrir os cinco problemas em cinco tentativas — e a maioria desiste no terceiro. É 422 e não 400: o corpo foi entendido; o que falhou foi o conteúdo, e um cliente consegue distinguir os dois casos.

Idempotência — o problema do checkout#

A resposta se perde na rede, o cliente reenvia, e a cobrança acontece de novo. O cliente sozinho não tem como saber; quem precisa reconhecer o reenvio é o servidor:

text
POST /cobrar
Idempotency-Key: pedido-8f2c

→ {"cobranca": 1}

POST /cobrar                    // mesma chave, cliente reenviou
Idempotency-Key: pedido-8f2c

→ {"cobranca": 1}               // Idempotent-Replay: true

Cache e compressão#

Kiln.cache(120) põe Cache-Control e ETag, e devolve 304 quando o cliente já tem a versão. Kiln.comprimir() faz gzip quando o cliente aceita e o corpo compensa — numa resposta JSON de 5,4 KB, 69 bytes na rede.

Ele não toca em imagem, vídeo nem zip: já estão comprimidos, e passar gzip por cima costuma aumentar o tamanho. Abaixo de 1 KB também não vale — o cabeçalho do gzip sozinho tem 18 bytes.

Comparado ao que você conhece#

KilnFlaskExpressFastify
declararserver api on 8080:Flask(__name__)express()fastify()
rotaroute GET "/x":@app.route("/x")app.get("/x", fn)f.get("/x", fn)
responderrespond json dreturn jsonify(d)res.json(d)return d
parâmetroparams["id"]<int:id>req.params.idreq.params.id
subirignite apiapp.run()app.listen()f.listen()
dependênciasnenhumaWerkzeug, Jinja2…npmnpm

Por onde seguir#

PáginaCobre
Rotas e parâmetros:id, *resto, os seis atalhos, 405
Respostasrespond, status, cookies, arquivos
Páginas HTMLtemplates, laços, escape automático
MiddlewareCORS, autenticação, limite de taxa
Sessão e cookieslogin, cookie assinado
Estáticos e uploadsCSS, imagens, downloads
Levar para produçãoo que muda fora da sua máquina
Referênciaas 46 funções do módulo