Dados e API
Explica **onde
cada coisa mora** e por quê — a pergunta mais comum de quem chega no projeto é "isso é
fetchdireto ou React Query? Vai emservices/ou emlib/?". Este documento existe pra nunca precisar adivinhar.
A regra de uma frase
fetch só existe dentro de services/*.ts. Todo componente fala com dado
remoto através de um hook do React Query, nunca chamando fetch ou uma
função de services/ direto.
componente (.tsx)
│ usa
▼
hook (useQuery / useMutation) ← features/<feature>/hooks/ ou shared/hooks/
│ chama
▼
função de services/*.ts (fetch puro) ← nenhum React aqui dentro
│ HTTP
▼
Rails api/
Por quê: cache, invalidação, estado de loading/error e retry já vêm de
graça do React Query — reimplementar isso com useState/useEffect em
cada componente é exatamente o problema que a lib resolve (ver
tecnologias/react-query.md). Deixar fetch
isolado em services/ também é o que permite trocar de biblioteca de
data-fetching no futuro sem tocar em componente nenhum.
services/ — só fetch, zero React
Cada arquivo fala com uma fatia coerente do contrato do backend, não
com "um recurso": services/api-admin.ts é genérico pra qualquer
/api/v1/admin/<recurso> (todo recurso nasce do mesmo gerador Rails, ver
api/CLAUDE.md), enquanto api-identity.ts e api-institucional.ts são
específicos porque /auth/* e /c_configuracoes/* têm formato próprio
(token no header, endpoint público sem sessão, FormData em vez de JSON).
| Arquivo | Fala com | Particularidade |
|---|---|---|
services/api-admin.ts | /api/v1/admin/* (qualquer recurso) | Genérico: adminList/Get/Create/Update/Delete recebem o nome do recurso como string |
services/api-identity.ts | /api/v1/auth/* | Modo fake sem NEXT_PUBLIC_API_URL; token vem no header, não no body |
services/api-institucional.ts | /api/v1/c_configuracoes/* | atual é público (sem token); update manda FormData (upload de imagem), não JSON |
Toda função de services/ segue o mesmo formato de retorno/erro:
devolve o data já desembrulhado do envelope {status, message, data} do
Rails, e lança uma classe de erro própria (AdminApiError,
ConfiguracaoInstitucionalApiError) com .message pronto pra mostrar num
toast e .status pro código HTTP.
// services/api-admin.ts — genérico, qualquer recurso admin
export function adminList<T>(resource: string, params?: {...}): Promise<PagedResult<T>> {
return request<PagedResult<T>>(`/api/v1/admin/${resource}${queryString(params)}`);
}
export function adminCreate<T>(resource: string, body: Record<string, unknown>): Promise<T> { ... }
export function adminUpdate<T>(resource: string, id, body): Promise<T> { ... }
export function adminDelete(resource: string, id): Promise<void> { ... }
shared/hooks/use-admin-resource.ts — a ponte genérica
Todo recurso admin (usuarios, papéis, tipos de usuário, permissões,
configuração institucional) tem o mesmíssimo shape de CRUD porque nasce do
mesmo bin/rails g api_scaffold do lado da API. Em vez de escrever
useQuery/useMutation na mão em cada feature, use-admin-resource.ts
oferece 5 hooks genéricos por cima de services/api-admin.ts:
useAdminList<T>(resource, params, queryKey) // useQuery — lista paginada
useAdminGet<T>(resource, id, queryKey) // useQuery — um registro
useAdminCreate<T>(resource, invalidateKeys, entidade) // useMutation + toast + invalidate
useAdminUpdate<T>(resource, invalidateKeys, entidade) // idem, PATCH
useAdminDelete(resource, invalidateKeys, entidade) // idem, DELETE
useAdminCreate/Update/Delete já disparam o toast de sucesso/erro
(shared/ui/sistema/toast.tsx) e invalidam as query keys passadas — nenhuma
feature repete essa lógica.
Hooks por feature — a camada de domínio
Cada features/<domínio>/<feature>/hooks/ chama os hooks genéricos acima,
já resolvendo o nome do recurso Rails, o corpo esperado ({a_papel: {...}})
e a query key:
// features/admin/tipos-usuario/hooks/use-create-tipo-usuario.ts
export function useCreateTipoUsuario() {
const mutation = useAdminCreate<ATipoUsuario>("a_tipos_usuario", [tiposUsuarioKeys.all], "Tipo de usuário");
function createTipoUsuario(valores: TipoUsuarioFormValues) {
return mutation.mutateAsync({ a_tipo_usuario: valores });
}
return { createTipoUsuario, ...mutation };
}
Regra de nomenclatura: um hook por operação (use-<verbo>-<entidade>.ts),
nunca um hook "canivete suíço" que decide internamente se cria ou atualiza —
essa decisão fica no componente de formulário (if (registro) update; else create).
Quando um recurso precisa de dois formatos (lista paginada pra tela de
listagem × lista completa pra dropdown, caso de a_tipos_usuario): dois
hooks, nomes diferentes, mesma services/api-admin.ts por trás —
useTiposUsuario() (sem paginação, pro <select> do formulário de usuário)
e useTiposUsuarioPaginado(page, busca) (pra tela /tipos-usuario). Nunca
mude o hook existente pra "fazer as duas coisas" — quebra quem já depende
dele.
types/, schemas/, constants/ — o resto da pasta de uma feature
Toda feature de admin segue a mesma forma:
features/admin/<feature>/
types/index.ts # o shape que a API devolve (espelha o serializer Rails)
constants/query-keys.ts # as query keys do React Query, centralizadas
schemas/<algo>.schema.ts # validação Zod do formulário (ver [Formulários](/padrao-frontend/formularios))
hooks/use-<algo>.ts # um arquivo por hook (list/get/create/update/delete)
components/<algo>-form.tsx, <algo>-list.tsx
types/nunca inventa campo — espelha literalmente o*_serializer.rbdo lado da API (comentário no topo do arquivo aponta o serializer real). Se o serializer não devolveemail, o type do front também não tememail.constants/query-keys.tsé a fonte única de verdade da query key — tanto o hook de leitura quanto o de mutação (invalidateKeys) importam daqui, nunca escrevem o array na mão duas vezes.schemas/valida só o que o backend também valida — nunca uma regra mais forte "pra garantir" (ex.:nomenão ganhamin(2)se o model Rails só exige presence). VerFORMULARIOS.md.
lib/ — utilitário puro, sem fetch e sem componente
lib/ é pra função pura (input → output, sem useState, sem fetch, sem
JSX). Se a função precisa de rede, é services/; se precisa de React, é um
hook ou componente.
| Arquivo | Faz |
|---|---|
lib/auth.ts | Lê/grava token e usuário no localStorage (getAccessToken, storeUser, clearStoredSession) |
lib/routes.ts | Constantes de rota + redirectSeguro() (bloqueia open redirect em ?redirect=) |
lib/error-utils.ts | extrairMensagem(payload, fallback) — lê .message de um erro da API sem assumir o shape inteiro |
lib/cn.ts | cn() — className condicional (ver tecnologias/clsx-tailwind-merge.md) |
lib/avatar.ts | getInitials/getAvatarColors — iniciais e cor estável a partir de um nome, pro avatar do topbar |
lib/auth.ts e lib/error-utils.ts são consumidos só por services/*.ts
(nunca por componente direto) — são o "sub-nível" que a camada de services
usa pra montar o header Authorization e ler mensagem de erro do envelope.
lib/cn.ts e lib/avatar.ts já são consumidos por componente direto, por
serem puramente de apresentação.
Leitura de apoio
- TanStack Query — Overview (docs) — por que
fetchfica emservices/e o componente fala via hook. - React Query as a State Manager (TkDodo) — cache de server state como fonte de verdade.
- Zod — Docs — schema de validação espelhado do backend.

