Backend Showcase
API REST de referência em Hono: login por JWT stateless, três CRUDs em memória, relatórios em GraphQL e no verbo QUERY, e uma spec OpenAPI 3.1 que nasce dos schemas Zod e alimenta quatro documentações. O mesmo código roda no Node e no edge da Cloudflare.
O Backend Showcase é o par do Frontend Showcase: a mesma ideia de estrutura de referência pronta para dar fork, agora do lado do servidor. É uma API (Application Programming Interface, interface que expõe as operações do sistema por HTTP) escrita em Hono, com login por token, três CRUDs (Create, Read, Update, Delete, as quatro operações básicas sobre um recurso), relatórios em duas linguagens de consulta e uma documentação que se escreve sozinha a partir do código. A intenção é servir de ponto de partida real para um produto novo, não de exemplo isolado de tutorial.
A escolha de não usar banco de dados é proposital. Os dados vivem em memória (arrays que nascem com cinco registros de exemplo cada e somem quando o servidor reinicia), a senha é fixa e o segredo do token fica no código. Nada disso vai para produção. O foco é mostrar a forma do backend, ou seja, como as rotas, a validação, a autenticação e a documentação se encaixam, sem que a configuração de um banco real roube a atenção do que importa.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| API (Application Programming Interface) | Camada que expõe as operações do sistema por HTTP, para outros programas consumirem. |
| REST (Representational State Transfer) | Estilo de API organizado em recursos (/users, /customers) e verbos HTTP (GET, POST, PUT, DELETE). |
| JWT (JSON Web Token) | Token assinado que carrega a identidade de quem fez login; o servidor confere a assinatura sem guardar sessão. |
| stateless (sem estado) | O servidor não guarda quem está logado; toda a informação viaja dentro do próprio token, então ele continua válido depois de um reinício. |
| OpenAPI | Documento padronizado que descreve a API inteira (rotas, parâmetros, respostas) de forma que ferramentas conseguem ler. |
| schema Zod (esquema de validação) | Descrição em código do formato esperado de cada dado; valida a entrada e, aqui, também gera a spec OpenAPI. |
| CRUD (Create, Read, Update, Delete) | As quatro operações básicas sobre um recurso: criar, ler, atualizar e remover. |
| envelope (formato fixo de resposta) | Toda resposta sai com as mesmas chaves na mesma ordem, então o cliente escreve o tratamento uma vez e ele serve para a API inteira. |
| traceId (identificador de rastreio) | Código único gerado por requisição, devolvido no corpo e no cabeçalho, que casa a resposta do usuário com a linha de log do servidor. |
| GraphQL | Linguagem de consulta em que quem chama escolhe os campos que quer de volta, num endpoint só. |
| QUERY (verbo HTTP) | Método de leitura recente, igual ao GET, com a diferença de carregar um corpo; serve para filtros que não cabem na URL. |
| seed (semente) | Dados iniciais carregados em memória quando a API sobe, para as listagens já virem preenchidas. |
| Vertical Slice (fatia vertical) | Organização do código por recurso (users, customers, financials), não por camada técnica; cada pasta carrega tudo que o recurso precisa. |
| rate limit (limite de requisições) | Trava que segura quantas chamadas um cliente pode fazer num intervalo, para proteger a API de abuso. |
| edge (borda da rede) | Servidores espalhados pelo mundo que respondem perto do usuário; aqui, os Cloudflare Workers. |
Stack
| Tecnologia | Papel |
|---|---|
| Node.js 24 LTS | Runtime, LTS ativo durante 2026 |
| TypeScript 6 | Linguagem, modo strict com noUncheckedIndexedAccess |
| Hono 4 | Framework web leve, roda em Node, Workers, Bun e Deno |
| Zod 4 + @hono/zod-openapi | Validação de entrada e geração da spec OpenAPI |
| jose 5 | Assina e verifica o JWT (HS256), compatível com o edge |
| graphql 17 | Relatórios em que o cliente escolhe os campos |
| Vitest 3 | Testes de middleware, stores e rotas |
| Biome 2 | Lint e formatação numa ferramenta só |
| pnpm 11 | Gerenciador de pacotes |
| Wrangler 4 | Deploy em Cloudflare Workers |
| Tecnologia | Papel |
|---|---|
| Scalar | Documentação interativa (padrão da landing) |
| Swagger UI | Documentação clássica, servida por HTML estático |
| Redoc | Documentação em coluna única, boa para leitura |
| GraphiQL | Playground GraphQL com autocomplete lido do schema |
| Landing HTML | Página inicial com tema claro/escuro e os quatro cards de docs |
A escolha do Hono é o eixo do projeto: um framework pequeno, TypeScript-first, que roda igual no Node e no edge. O mesmo
appque sobe local emhttp://localhost:3000é o que a Cloudflare publica. Sem reescrever nada.
Arquitetura: fatia vertical
O código segue Vertical Slice: cada recurso guarda a própria rota e o próprio store na mesma pasta, em vez de espalhar tudo em camadas técnicas separadas (uma pasta de “controllers”, outra de “services”). Quem vai mexer no financeiro abre features/financials/ e encontra ali tudo que o recurso precisa.
| Padrão | Descrição | Detalhes |
|---|---|---|
| Fatia por recurso | features/users, /customers, /financials, /reports: cada uma com rota + store + testes | Uma pasta nova por recurso; nenhuma conhece a outra por import cruzado |
| App único e portátil | app.ts monta tudo; index.ts sobe no Node e worker.ts sobe no edge | O mesmo objeto Hono, dois pontos de entrada; nada de código duplicado |
| Contrato congelado em /api/v1 | Todos os recursos vivem sob o prefixo de versão | Mudança incompatível estreia como /api/v2 e convive lado a lado, sem migração forçada |
| Stores em memória | Arrays singleton com 5 registros de seed, ids em UUIDv7 ordenável por tempo | Zero banco: o foco é a forma da API, não a persistência |
| Envelope único | succeed, succeedPage e failWith montam toda resposta no mesmo formato | As chaves saem sempre na ordem ok, meta, payload |
| Log estruturado com traceId | Um middleware registra método, rota, status e duração em JSON por request | O mesmo traceId volta no corpo e no cabeçalho X-Trace-Id; sem dependência externa |
1. Login por JWT stateless
A autenticação prova que dá para ter um fluxo de login inteiro sem banco e sem sessão guardada no servidor. O usuário manda e-mail e senha para POST /api/v1/auth/login; se batem com o seed, a rota devolve um JWT assinado, válido por uma hora. Da próxima chamada em diante, o token viaja no cabeçalho Authorization: Bearer ... e um guard confere a assinatura antes de deixar passar. O POST /api/v1/auth/logout encerra a sessão do lado do cliente.
O detalhe que importa é o stateless. O servidor não anota quem está logado em lugar nenhum; toda a identidade mora dentro do token assinado. A consequência prática é que o token continua valendo depois de reiniciar o servidor, porque não havia nada guardado para se perder. A biblioteca jose (JavaScript Object Signing and Encryption) faz a assinatura em HS256, um algoritmo que funciona tanto no Node quanto no edge.
# 1. faz login e recebe o token
curl -sX POST http://localhost:3000/api/v1/auth/login \
-H 'content-type: application/json' \
-d '{"email":"[email protected]","password":"showcase"}'
# 2. usa o token nas rotas protegidas
curl -s http://localhost:3000/api/v1/users \
-H 'authorization: Bearer SEU_TOKEN'
Qualquer e-mail do seed funciona com a senha showcase. Dentro de qualquer documentação, o botão Authorize cola o token para você testar as rotas pelo navegador.
2. Três CRUDs e uma resposta com formato fixo
Sob o prefixo /api/v1 ficam três recursos completos: users, customers e financials. Cada um responde às cinco rotas de um CRUD (listar, criar, buscar por id, atualizar, remover) e todas exigem o token Bearer. Repetir o mesmo formato três vezes é intencional: mostra que o padrão se copia para um recurso novo sem surpresa. O GET /health fica de fora do prefixo, porque é status de operação e não parte do contrato.
Toda resposta sai no mesmo envelope, com as chaves na ordem ok, meta, payload. O campo ok diz de cara qual caminho seguir, e o payload se chama data no sucesso e error na falha. O campo que não se aplica fica de fora, sem data: null sobrando:
{
"ok": true,
"meta": {
"route": "GET /api/v1/users/123",
"action": "users.detail",
"traceId": "a1b2c3d4-..."
},
"data": { }
}
O meta.traceId é um identificador único do request, repetido no cabeçalho X-Trace-Id e na linha de log. Num incidente, o usuário mostra o código da resposta e o servidor entrega a linha exata que o produziu.
As listagens aceitam paginação por ?page= e ?pageSize= (padrão page=1, pageSize=10, teto de 100). Os números da página aparecem no meta, antes do data, de modo que a leitura corre na ordem natural, “página X de Y, tantos por página, tantos no total”:
{
"ok": true,
"meta": { "page": 1, "totalPages": 5, "pageSize": 10, "totalItems": 42 },
"data": [ ]
}
Esse contrato nasce de um schema Zod compartilhado, reaproveitado pelas três rotas de listagem. Definir uma vez e reusar evita que cada recurso invente o próprio jeito de paginar. A montagem do envelope mora em src/lib/envelope.ts, em três funções (succeed, succeedPage, failWith) que as rotas chamam no lugar de responder à mão.
3. Uma spec OpenAPI, quatro documentações
A documentação é o ponto onde o projeto mais economiza trabalho. Em vez de escrever a OpenAPI à mão (e vê-la envelhecer a cada rota nova), cada rota é anotada com .openapi() e descrita pelo mesmo schema Zod que já valida a entrada. A spec 3.1 nasce do código em tempo de execução e fica em /openapi.json. Uma fonte, sempre em dia com a implementação.
Dessa spec única saem quatro interfaces, servidas lado a lado para quem prefere cada estilo:
| Padrão | Descrição | Detalhes |
|---|---|---|
| Scalar | Documentação interativa e moderna, a opção padrão da landing | /docs/scalar |
| Swagger UI | A interface clássica que a maior parte dos times já conhece | /docs/swagger |
| Redoc | Leitura em três painéis, sem executar requisição | /docs/redoc |
| GraphiQL | Playground do endpoint GraphQL, com autocomplete lido do próprio schema | /docs/graphql |
A rota / abre uma landing com os quatro cards, tema claro/escuro e a versão lida direto do package.json, então a página nunca diverge do que está publicado. As introduções das docs explicam envelope, paginação, rastreabilidade e códigos de erro em accordions.
4. Relatórios: GraphQL e o verbo QUERY
Relatório é o tipo de leitura que o GET comum não resolve bem. Cada tela quer um recorte diferente, e o filtro nem sempre cabe na URL. O projeto responde com dois caminhos, ambos protegidos pelo mesmo token Bearer.
O primeiro é GraphQL em POST /api/v1/reports/graphql: um endpoint só, no qual quem chama escolhe os campos que quer de volta. Serve quando telas diferentes precisam de recortes diferentes do mesmo relatório, sem abrir uma rota para cada uma. A resposta sai no contrato do próprio GraphQL (data no sucesso, errors na falha), fora do envelope das rotas REST. Essa exceção é deliberada, porque cada protocolo mantém a forma que já se espera dele.
O segundo é o verbo QUERY em QUERY /api/v1/reports, um método HTTP em rascunho na IETF. Ele é uma leitura segura, igual ao GET, com uma diferença: carrega um corpo. Assim o filtro grande não precisa caber na URL, não estoura o limite de tamanho e não vaza em log nem em cache.
curl -X QUERY http://localhost:3000/api/v1/reports \
-H 'authorization: Bearer SEU_TOKEN' \
-H 'content-type: application/json' \
-d '{"type":"expense","from":"2026-01-01","to":"2026-01-31"}'
O QUERY não aparece na lista de rotas das UIs de documentação, e a razão é uma limitação de formato: o Path Item do OpenAPI 3.1 não tem vaga para o método. Só o 3.2 ganhou o campo query, que Swagger UI e Redoc ainda não renderizam. Por isso ele fica documentado em prosa, com o exemplo acima.
5. Resiliência: rate limit e erros padronizados
Duas peças protegem a API de abuso e de respostas inconsistentes. O rate limit é um middleware em memória que libera cinco requisições por minuto, por cliente e por rota, numa janela deslizante. Ao estourar, devolve 429 RATE_LIMITED com o cabeçalho Retry-After e os X-RateLimit-*, o suficiente para o cliente saber quando pode tentar de novo. Navegar pela landing e pelas documentações não consome cota.
Do lado dos erros, um onError global garante que toda falha saia no mesmo formato, seguindo o Problem Details da RFC 9457. O error.status repete o status HTTP, o error.code é o identificador estável que a máquina lê, e o error.type aponta para /problems/<code>:
{
"ok": false,
"meta": { "route": "GET /api/v1/users/123", "traceId": "...", "timestamp": "2026-07-08T14:32:10.123Z" },
"error": {
"status": 404,
"title": "Not Found",
"detail": "User 123 not found",
"code": "NOT_FOUND",
"type": "/problems/not-found",
"instance": "/api/v1/users/123"
}
}
Erro de validação do Zod vira 400 INVALID_INPUT e ganha um error.errors com cada campo inválido. A lista fecha em cinco códigos (400, 401, 404, 429, 500), então o cliente trata erro uma vez e cobre a API inteira.
6. Do Node ao edge: deploy em Cloudflare Workers
O mesmo app Hono que roda no Node também roda nos Cloudflare Workers, os servidores da Cloudflare espalhados pelo mundo que respondem perto do usuário. O worker.ts só faz export default app, e o wrangler.jsonc liga a flag nodejs_compat (para o jose funcionar) e desativa os subdomínios de preview, deixando só o domínio próprio bs.thiagocaja.dev responder. Um push na main dispara o build e o deploy automático via Cloudflare Workers Builds.
O GET /health devolve status, versão publicada e há quanto tempo ela está no ar. Medir uptime no edge exige um truque: lá não existe processo de vida longa, o código roda em isolates que nascem e morrem o tempo todo, e um contador de processo marcaria zero para sempre. O valor real vem do binding version_metadata da Cloudflare, que carimba quando aquela versão foi publicada. Fora da Cloudflare não há deploy, então o campo vem nulo e a contagem começa na primeira requisição.
A última decisão de segurança é o CORS (Cross-Origin Resource Sharing, regra do navegador para chamadas entre domínios) por allowlist. No lugar de liberar qualquer origem com *, a API devolve o cabeçalho só para o próprio bs.thiagocaja.dev e para localhost em qualquer porta, que cobre o desenvolvimento. Origem de terceiro não recebe o cabeçalho e o navegador barra o fetch. Isso não atrapalha ninguém: as documentações são servidas do mesmo domínio, e CORS só existe dentro do navegador, então curl, Postman e consumo server-side seguem livres. Se um dia a autenticação virar cookie de sessão, o allowlist deixa de ser escolha e vira obrigação, porque Access-Control-Allow-Credentials: true não combina com *.
Convenções
- Código em inglês: identificadores, nomes de arquivo e rotas. Quem chega no projeto não traduz termos no meio da leitura.
- Mensagens em pt-BR: descrições das rotas na OpenAPI, textos da landing e mensagens de erro conversam com quem lê.
- Arquivos nomeados por recurso e papel (
auth.middleware.ts,users.store.ts,users.route.ts). - Uma ferramenta por tarefa: Biome cobre lint e formatação juntos; Vitest cobre os testes das peças frágeis (guard, stores, rotas).
Status
| Aspecto | Estado |
|---|---|
| Maturidade | Estrutura pronta para fork, evolução iterativa |
| Cobertura de testes | Unit (Vitest) em middleware, stores e rotas |
| Disponibilidade | Live em Cloudflare Workers (bs.thiagocaja.dev) |
| Repositório | Privado, código sob solicitação |
| Monetização | Nenhuma, projeto de portfólio |
Backend e frontend formam um par: um serve o contrato REST com auth, paginação e documentação viva; o outro consome esse contrato numa interface React. Juntos mostram a mesma aplicação de ponta a ponta, cada lado como um artefato que se sustenta sozinho.