ADR-0036 Camada de API
Camada de API HTTP padrão da empresa — server routes do TanStack Start em /api/*, com os handlers em src/server/api/ e autenticação só por token. Hono apenas quando um produto publica um contrato de API.
- Decisão
- Server routes do Start em /api/*Construir a app
- Estado
- Aceite
- Data
- Âmbito
- Predefinição da empresa para todas as novas apps
Ligações
Apoia-se em
Contexto
Seção intitulada “Contexto”O ADR-0004 pôs o Hono em /api/*, montado por uma única rota splat dentro do servidor TanStack Start, e escolheu-o em vez das server routes do próprio Start por causa das APIs standard da web, de um ecossistema de middleware HTTP, de middleware de validação Zod, de um caminho para OpenAPI e de uma extração limpa para um serviço autónomo.
Desde então:
- As server routes do Start alcançaram o middleware. Uma rota de ficheiro aceita
server.middlewarepara todos os métodos e middleware por método através decreateHandlers— o mesmocreateMiddlewareque as server functions e osrc/start.tsusam. Os handlers recebem umRequeststandard da web e devolvem umaResponse, como os do Hono - O que o
/api/*serve na prática é o handler do Better Auth, webhooks e alguns endpoints para uma app móvel ou um script. Poucos produtos publicam um contrato de API a parceiros - O Hono tem um custo corrente dentro do Start: um segundo router e um segundo sistema de middleware, uma rota splat para os ligar, spans de servidor duplicados do
@hono/otel, uma regra contra orequestId()do Hono criar um segundo ID (ADR-0015), e o middleware CSRF do próprio Hono para as rotas de IA (ADR-0025) - O ADR-0027 diz YAGNI: a geração de OpenAPI e um caminho de extração só justificam uma framework num produto que precise deles
O que está em aberto: o que serve /api/* por predefinição, e como os seus endpoints validam, autenticam e são testados.
Opções consideradas
Seção intitulada “Opções consideradas”1. Manter o Hono (ADR-0004)
Seção intitulada “1. Manter o Hono (ADR-0004)”Prós: @hono/zod-openapi para uma spec e clientes gerados; um cliente tipado (hc); middleware de CORS, secure-headers e validação incluído; a API extrai-se para o seu próprio serviço sem alterações.
Contras: dois routers e dois sistemas de middleware em cada app, a cola entre eles, telemetria duplicada e convenções para explicar — pago por todos os produtos, usado por poucos.
2. Server routes do Start
Seção intitulada “2. Server routes do Start”Prós
- Um só router: os endpoints da API são rotas de ficheiro, na árvore de rotas, com os seus padrões em
http.route - Um só sistema de middleware para páginas, server functions e rotas da API
- Sem dependência; a validação é
safeParsecom os schemas que já partilhamos (ADR-0005)
Contras
- Sem geração de OpenAPI nem clientes gerados
- Sem middleware de CORS; escreve-se um quando um produto precisar
- Acoplado ao Start: mover a API para o seu próprio serviço implica reescrever a ligação das rotas
3. tRPC
Seção intitulada “3. tRPC”Prós: RPC tipado com boa ergonomia de TypeScript. Contras: faz o que as server functions já fazem para a nossa UI, e não é um contrato REST para clientes externos — um terceiro padrão de servidor sem ganho.
Decisão
Seção intitulada “Decisão”As server routes do TanStack Start em /api/* são a norma da empresa para endpoints HTTP; as server functions continuam a ser a forma como a nossa UI fala com o servidor. Um produto só acrescenta o Hono através de um ADR próprio, quando publica um contrato de API (OpenAPI, clientes gerados) para parceiros ou apps móveis.
Dois padrões de servidor, uma regra (inalterado desde o ADR-0004):
- Server functions → carregamento de dados e mutações apenas para a nossa própria app React
- Server routes em
/api/*→ tudo o que seja consumido como HTTP: Better Auth, webhooks, apps móveis, parceiros, scripts
Em concreto:
-
Os ficheiros de rota só fazem a ligação; os handlers vivem em
server/api/(ADR-0026). Um ficheiro de rota mapeia métodos para handlers, e os handlers fazem parse, autorizam e chamam o serviço:apps/web/src/routes/api/v1/invoices.ts import { createFileRoute } from "@tanstack/react-router";import { createInvoice, listInvoices } from "@/server/api/v1/invoices";export const Route = createFileRoute("/api/v1/invoices")({server: { handlers: { GET: listInvoices, POST: createInvoice } },});// apps/web/src/server/api/v1/invoices.ts (excerto)export const createInvoice: ApiHandler = async ({ request, context }) => {const input = await readJson(request, invoiceInput);if (input instanceof Response) return input; // 415 ou 400, a mesma forma em todas as rotas// autorizar (ADR-0033) e depois chamar o serviçoreturn Response.json(await invoices.create(context.caller, input), { status: 201 });}; -
Caminhos versionados:
/api/v1/<domain>, no plural como a tabela e a página. Uma alteração incompatível é uma nova rota/api/v2/, e a antiga fica até os seus clientes terem migrado -
Validação:
readJson(request, schema)ereadQuery(url, schema)emserver/api/http.tsverificamContent-Type: application/json(415caso contrário), fazem parse e validam com o schema Zod partilhado. JSON malformado e um schema que falha respondem ambos400comz.flattenError(error), pelo que todas as rotas falham da mesma forma -
Os callers da API autenticam-se com um token, nunca com o cookie de sessão. O
apiCaller, um middleware de pedidos emsrc/start.ts, corre para todos os caminhos versionados (/api/v…). Passa apenas o cabeçalhoAuthorizationao Better Auth — o plugin bearer ou API-key, escolhido por produto (ADR-0009) — põe o caller nocontext, e responde401quando não há nenhum. Um browser nunca anexa esse cabeçalho por si, pelo que uma página de outro site não consegue fazer uma chamada autenticada, e estas rotas não precisam de verificação CSRF; as escritas da nossa UI passam por server functions, que têm uma (ADR-0013). Cada handler verifica depois uma permissão (ADR-0033) -
O Better Auth é montado por
routes/api/auth/$.ts, cujosGETePOSTentregam o pedido aauth.handler(request)— a configuração documentada do Better Auth para o TanStack Start. Verifica ele próprio as origens (trustedOrigins) -
Os webhooks vivem em
/api/webhooks/<provider>, fora dos caminhos versionados, e verificam a assinatura do fornecedor sobre o corpo em bruto antes de fazer parse -
Sem CORS por predefinição — a nossa UI é da mesma origem. Um produto cuja API serve browsers noutras origens acrescenta um middleware de CORS com uma lista explícita de origens, nunca
*com credenciais -
O rate limiting, os IDs de pedido e o tracing vêm de
src/start.ts, como em todos os pedidos (ADR-0010, ADR-0015); os spans têm o nome do padrão da rota, comoPOST /api/v1/invoices -
Testes: os testes de integração chamam os handlers em
server/api/v1/com umRequeste um caller, contra a base de dados real (ADR-0014); o E2E cobre a cadeia de middleware à frente deles -
Um produto que publica um contrato escreve um ADR que adota o Hono com
@hono/zod-openapisob o prefixo dessa API, ou gera a spec a partir dos schemas Zod partilhados
Os fatores decisivos: um só router e um só sistema de middleware em cada app, nada instalado para o que o Start já faz, e o OpenAPI pago apenas pelos produtos que publicam uma API. Aceitamos escrever nós dois pequenos helpers e perder um caminho de extração que nenhum produto usou.
Consequências
Seção intitulada “Consequências”Mais fácil:
- Páginas, server functions e endpoints da API são todos rotas de ficheiro atrás de uma só cadeia de middleware; nada liga duas frameworks
- As rotas da API aparecem na árvore de rotas, e os seus padrões dão nome a spans e métricas sem nenhum pacote extra
- Um pacote a menos para manter atualizado, e sem regras específicas do Hono para IDs de pedido, CSRF ou telemetria
Mais difícil:
- Sem spec OpenAPI por predefinição: um produto que mais tarde publique uma API adota então o Hono ou um gerador, num ADR próprio
- Mover a API para um serviço autónomo implica reescrever a ligação das rotas; os handlers são funções simples em
server/api/, pelo que a reescrita fica fina - O
apiCallertem de corresponder a todos os caminhos versionados; um teste de guarda chama uma rota/api/v1/com um cookie de sessão e sem token e espera401 - O CORS, quando um produto o precisar, é nosso para escrever e rever
Do que abdicamos: do catálogo de middleware do Hono e do seu caminho para OpenAPI como predefinições, e de extrair a API sem uma reescrita. Aceite — um produto que precise deles di-lo no seu próprio ADR, e todos os outros produtos correm uma framework em vez de duas.

