Pular para o conteúdo principal
Versão: main (dev) 🚧

ADR-007 — Frontend SPA servido same-origin pela API (monorepo, shadcn/ui)

Date: 2026-06-12 Status: Accepted Pareado com: ADR-006 · brainstorm docs/brainstorms/2026-06-12-frontend-monorepo-shadcn-brainstorm.md

Context

O template passou a nascer com autenticação BFF same-origin (ADR-006): o browser usa um cookie de sessão HttpOnly + X-XSRF-TOKEN, sem token em JavaScript. Esse modelo só funciona se o SPA for servido na mesma origem da API — caso contrário, o cookie vira cross-site (exigindo SameSite=None + CORS credenciado, enfraquecendo a postura CSRF).

O template precisa de um frontend de referência (paridade com o canon backend, onde Orders é referência viva) usando shadcn/ui (React + Tailwind), entregue em monorepo com o backend.

Decision

Um único repositório (o próprio ITLab.Template.Next) com o SPA servido same-origin pela API.

  • Projeto: src/ITLab.Template.Next.Spa — Vite + pnpm + React + TypeScript + Tailwind + shadcn/ui. Nome .Spa (não .Web, que é ambíguo: a API também é web).
  • Serving: a API serve o SPA. Produção: UseStaticFiles() + MapFallbackToFile("index.html") (após os endpoints /v1/*, /.well-known/*, /health/*, que não são capturados pelo fallback). Desenvolvimento: Microsoft.AspNetCore.SpaProxy (ou server.proxy do Vite) faz uma única origem frontear o dev server do Vite + a API.
  • Build integrada: o Api.csproj dispara pnpm install + pnpm build no publish/Release, copiando distwwwroot. Um único artefato, um único deploy.
  • Sem tool de monorepo JS (Nx/Turborepo): é um único app de front — YAGNI. pnpm puro.
  • API client: openapi-typescript (tipos a partir do OpenAPI 3.1 já exposto) + openapi-fetch. Stack de dados: TanStack Query; forms: react-hook-form + zod; routing: React Router.
  • Canon de UI: auth (login/logout/guarda de rota/CSRF/GET /v1/auth/me) + Orders (list/create/approve, role-gated) como referência viva.

Consequences

Positivas:

  • O cookie BFF do ADR-006 funciona sem CORS nem hacks de SameSite — same-origin por construção.
  • Um deploy, um artefato; dotnet run/dotnet publish cobrem back+front.
  • Tipos da API ponta-a-ponta via OpenAPI; o front acompanha o contrato do backend.

Negativas:

  • O build do .NET passa a depender de Node + pnpm (CI precisa do toolchain JS).
  • dotnet new precisa templatizar o SPA (rename do sourceName, excluir node_modules/dist) — trabalho da Fase 3 de empacotamento (TODO FE-09).
  • Acopla o ciclo de build do front ao da API (mitigado: SpaProxy só em dev; build do front só no publish/Release).

Alternatives considered

OpçãoPor que descartada
Hosting separado (SPA em CDN/static host) + reverse proxy unificando origemMais partes móveis (gateway, 2 deploys) para o mesmo resultado de origem; só compensa se o front precisar escalar/CDN independente.
Origens separadas + CORS credenciadoQuebra o BFF puro (SameSite=None, CSRF mais fraco) — contraria o ADR-006.
Nx/TurborepoPesado para um único SPA + um backend .NET; mais conceitos para o consumidor manter.
Cliente tipado à mão (sem codegen)Tipos divergem da API com o tempo; o OpenAPI já existe e dá geração barata.

References

  • ADR-006 — Local Identity dual-scheme (por que same-origin).
  • docs/brainstorms/2026-06-12-frontend-monorepo-shadcn-brainstorm.md.
  • docs/todos/2026-06-12-todos-frontend-spa.md (plano FE-01..FE-10).

Adendo de implementação (2026-06-12, v1.0.1 — não altera a decisão). O mecanismo de dev evoluiu em relação às opções cogitadas acima: o caminho primário é um reverse proxy do próprio host ASP.NET para o Vite dev server (Microsoft.AspNetCore.SpaServices.Extensions, ativado por Spa:DevServerUrl), navegando em https://localhost:52106 — mesma origem da produção, HMR via websocket proxiado, prefixos de API fora do proxy. O server.proxy do Vite permanece como fluxo alternativo (apontando para o endpoint HTTPS — o HTTP dispara o 307 do UseHttpsRedirection e quebra o cookie BFF). Microsoft.AspNetCore.SpaProxy foi descartado (redireciona o browser para a origem do Vite em vez de proxiar). Ver CHANGELOG.md § 1.0.1 e Program.cs § SPA serving.