ADR-0013 Formulários
Formulários padrão da empresa — TanStack Form com schemas Zod partilhados, componentes Field do shadcn/ui e server functions POST; JavaScript obrigatório por predefinição.
- Decisão
- TanStack Form + Zod + shadcn/ui FieldConstruir 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”Todos os produtos têm formulários: início de sessão, definições, ecrãs de criação/edição, fluxos com vários passos. As decisões à volta estão tomadas:
- Validação: Zod 4, um schema por forma, partilhado entre cliente e servidor (ADR-0005) — que já assume o TanStack Form
- Componentes: shadcn/ui sobre Base UI (ADR-0011); os componentes
Fielddo shadcn/ui (Field,FieldLabel,FieldDescription,FieldError) trazem guias para o TanStack Form e o React Hook Form - Escritas: server functions
POSTchamadas a partir deuseMutation, seguidas de invalidação (ADR-0012); toda a escrita passa pelo limitador (ADR-0010) - Os formulários de autenticação chamam o SDK cliente do Better Auth (ADR-0009), que precisa de JavaScript
O que está em aberto: a biblioteca de estado de formulários, como os erros chegam ao utilizador e se os formulários têm de funcionar antes de o JavaScript carregar.
Dois factos do Start moldam a resposta:
- As server functions são endpoints. Qualquer pessoa as pode chamar diretamente, não só o nosso formulário — por isso o servidor valida e verifica a autenticação em cada chamada; a validação no cliente é UX, nunca proteção
- A proteção CSRF para server functions vem de
createCsrfMiddleware(), que verificaSec-Fetch-Site,OrigineReferer. O Start instala-o automaticamente apenas quando a app não temsrc/start.ts— e todas as apps têm um, por causa do limitador (ADR-0010)
Opções consideradas
Seção intitulada “Opções consideradas”1. TanStack Form
Seção intitulada “1. TanStack Form”Prós
- Mesma equipa do Router, do Query e do Start; MIT; 1.x estável (1.33.5)
- Adaptador oficial para o Start (
@tanstack/react-form-start) para validação no servidor e submissões sem JS quando precisarmos - Os schemas Zod 4 encaixam diretamente através do Standard Schema — sem pacote de resolver
createFormHookliga os nossos próprios componentes de campo uma só vez; os formulários passam então a usarform.AppFieldcom nomes de campo tipados — uma gralha é um erro de compilação- Subscrições de granularidade fina: uma tecla premida volta a renderizar apenas o campo, não o formulário
Contras
- Ecossistema mais pequeno e menos exemplos do que o React Hook Form
- A API de render props (
children={(field) => …}) é mais verbosa semcreateFormHook - A v2 está em alpha; vem aí uma atualização major
2. React Hook Form
Seção intitulada “2. React Hook Form”Prós
- O maior ecossistema de formulários e mais exemplos; o shadcn/ui tem um guia para ele; 7.x estável (7.90.0)
- Zod via
@hookform/resolvers(6.x, suporta Zod 4)
Contras
- Sem integração com o Start; a validação no servidor e a submissão sem JS ficam por nossa conta
- A v8 está em beta — uma atualização major vem aí em breve
- Um segundo pacote adaptador de validação; o pressuposto do ADR-0005 de que usamos TanStack Form deixaria de valer
3. Conform
Seção intitulada “3. Conform”Prós: construído em torno do progressive enhancement e do FormData; suporte para Zod 4.
Contras: pensado para server actions do Remix e do Next.js; sem adaptador para o TanStack Start e sem guia do shadcn/ui.
4. Sem biblioteca (formulários nativos + useActionState do React 19)
Seção intitulada “4. Sem biblioteca (formulários nativos + useActionState do React 19)”Prós: zero dependências. Contras: erros por campo, estado touched/dirty, arrays e verificações assíncronas reconstruídos em cada produto.
Progressive enhancement
Seção intitulada “Progressive enhancement”- JS obrigatório: os formulários submetem através de
useMutation; ficam desativados até à hidratação. Um só caminho de código, alinhado com o ADR-0012 e com o SDK cliente do Better Auth - Progressivo por predefinição: todos os formulários fazem POST de
FormDataparaserverFn.urle funcionam sem JavaScript. Robusto em redes lentas, mas todos os formulários precisam de parsing deFormData, erros renderizados no servidor e redirecionamentos — e os formulários de autenticação não encaixam no SDK cliente
Decisão
Seção intitulada “Decisão”TanStack Form com schemas Zod partilhados e componentes Field do shadcn/ui é o padrão da empresa para formulários. Os formulários exigem JavaScript por predefinição; o progressive enhancement é opt-in por formulário.
Em concreto:
- Pacote:
@tanstack/react-form1.x. Acrescentar@tanstack/react-form-start(mesma versão) apenas para formulários sem JS - Um form hook por app:
createFormHookContexts()+createFormHook()emsrc/components/form/, ligando componentes de campo construídos sobre oFielddo shadcn/ui (TextField,TextareaField,SelectField,CheckboxField,SwitchField, …) e componentes de formulário (SubmitButton,FormError). Os formulários usamuseAppFormeform.AppField; os sub-formulários usamwithForm. Os produtos estendem este conjunto — não ligamform.Fieldà mão em cada ecrã - Erros acessíveis, seguindo o guia do shadcn/ui:
data-invalidnoField,aria-invalidno controlo,FieldErrorrenderizado quandoisTouched && !isValid. Os componentes de campo tratam disto, pelo que os ecrãs não se podem esquecer - Um schema Zod por formulário, num módulo que ambos os lados importam (sem imports exclusivos do servidor). O cliente usa-o como validador do formulário (
validators: { onSubmit: schema }; acrescentaronBlurem formulários longos). A server function valida com o mesmo schema:createServerFn({ method: "POST" }).validator(schema). O servidor valida sempre; a validação no cliente serve apenas para dar feedback mais rápido - Submeter através de uma mutação:
onSubmit: ({ value }) => mutation.mutateAsync(value), em que a mutação chama a server functionPOSTviauseServerFne invalida em caso de sucesso (ADR-0012). O botão de submissão mostraisSubmittinge impede submissões duplicadas - Os erros do servidor são mapeados de volta para os campos. As falhas de regras de negócio (email já registado, stock esgotado) devolvem erros indexados por campo a partir da server function e são renderizadas pelo mesmo
FieldError; os validadores ao nível do formulário do TanStack Form podem definir erros de campo ({ fields: { email: "…" } }). Os erros inesperados mostram uma única mensagem ao nível do formulário — nunca um stack trace nem um erro em bruto da base de dados - Toda a server function de escrita verifica ela própria a sessão (middleware de autenticação ou verificação no handler) — o
beforeLoadde uma rota protege a página, não o endpoint - CSRF:
src/start.tsregistacreateCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" })ao lado do limitador, porque definirsrc/start.tsdesativa o middleware automático do Start. Passar sempreorigin: APP_ORIGIN— atrás do reverse proxy, o URL do pedido não é uma fonte fiável para a origem pública (ADR-0017) - Os formulários com JS obrigatório são seguros antes da hidratação:
<form method="post">para que uma submissão nativa nunca possa pôr valores (palavras-passe) no URL, e o botão de submissão fica desativado até hidratar (useHydrated()de@tanstack/react-router) - Os formulários de autenticação usam o mesmo form hook e os mesmos componentes de campo; o
onSubmitchama o cliente do Better Auth (por exemploauthClient.signIn.email) em vez de uma server function e depois limpa as caches como no ADR-0012 - Os formulários sem JS são opt-in — para formulários públicos que têm de funcionar sem JavaScript (por exemplo, um formulário de contacto):
formOptionsecreateServerValidatedo@tanstack/react-form-start,<form action={serverFn.url} method="post" encType="multipart/form-data">, egetFormData+mergeFormpara mostrar os erros do servidor após o ciclo de ida e volta
Os fatores decisivos: a mesma equipa e o mesmo ciclo de lançamentos do Router, do Query e do Start; schemas Zod sem adaptador; componentes de campo tipados e pré-ligados que tornam os erros acessíveis a predefinição; e um único caminho de submissão através de server functions e mutações. Aceitamos o ecossistema mais pequeno e a próxima atualização para a v2.
Consequências
Seção intitulada “Consequências”Mais fácil:
- Um só schema valida o formulário, tipa os seus valores e protege a server function — cliente e servidor não conseguem divergir
- Os ecrãs compõem componentes de campo já prontos; rótulos, descrições e estados de erro acessíveis vêm sem esforço
- Toda a escrita segue o mesmo caminho: formulário → mutação → server function
POST→ limitador → verificação CSRF → validação → handler → invalidação
Mais difícil:
- O conjunto de componentes de campo em
src/components/form/é nosso para construir e manter; é o primeiro candidato para o registry partilhado do shadcn/ui (ADR-0011) - A maioria dos exemplos da comunidade usa React Hook Form; adaptá-los em vez de os copiar
- O
src/start.tstem de manter o middleware CSRF — removê-lo, ou restringir o seu filtro, abre em silêncio todas as server functions a pedidos cross-site. Acrescentar um teste que garanta que umPOSTcross-site é rejeitado - Os formulários não funcionam antes da hidratação em ligações lentas; o botão de submissão desativado torna isso visível em vez de silencioso
- O TanStack Form v2 está em alpha; planear uma janela de atualização quando sair
Do que abdicamos: do ecossistema do React Hook Form e do progressive enhancement como predefinição. Aceite — um só ciclo de lançamentos e um só caminho de submissão pesam mais numa predefinição da empresa; os produtos com requisitos de funcionamento sem JS fazem opt-in por formulário.

