Pular para o conteúdo

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

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.middleware para todos os métodos e middleware por método através de createHandlers — o mesmo createMiddleware que as server functions e o src/start.ts usam. Os handlers recebem um Request standard da web e devolvem uma Response, 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 o requestId() 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.

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.

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 é safeParse com 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

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.

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ço
    return 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) e readQuery(url, schema) em server/api/http.ts verificam Content-Type: application/json (415 caso contrário), fazem parse e validam com o schema Zod partilhado. JSON malformado e um schema que falha respondem ambos 400 com z.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 em src/start.ts, corre para todos os caminhos versionados (/api/v…). Passa apenas o cabeçalho Authorization ao Better Auth — o plugin bearer ou API-key, escolhido por produto (ADR-0009) — põe o caller no context, e responde 401 quando 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, cujos GET e POST entregam o pedido a auth.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, como POST /api/v1/invoices

  • Testes: os testes de integração chamam os handlers em server/api/v1/ com um Request e 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-openapi sob 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.

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 apiCaller tem 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 espera 401
  • 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.