Skip to content

Estado da Arte — Padrões Obrigatórios (v1.0)

Documento-guia consolidado com todos os padrões mandatórios para novas features e manutenção. Atualizado: 2026-03-26


1. Backend (Edge Functions)

1.1 Template Obrigatório

Toda Edge Function deve seguir o template:

typescript
import { handleCorsPrelight, getCorsHeaders } from '../_shared/cors-helpers.ts';
import { extractAuthenticatedUser } from '../_shared/auth-helpers.ts';
import { createSupabaseClient } from '../_shared/supabase-client.ts';
import { registrarLog } from '../_shared/logging-helper.ts';

Deno.serve(async (req: Request) => {
  if (req.method === 'OPTIONS') return handleCorsPrelight(req);
  const corsHeaders = getCorsHeaders(req);
  
  try {
    const { action, ...payload } = await req.json();
    
    // Ações públicas ANTES do auth check
    if (action === 'public_action') { /* ... */ }
    
    // Auth check
    const user = await extractAuthenticatedUser(req);
    if (!user) return new Response(JSON.stringify({ success: false, message: 'Não autenticado' }), { status: 200, headers: corsHeaders });
    
    const supabase = createSupabaseClient(req);
    
    // Roteamento por ação
    if (action === 'list') { /* ... */ }
    
    return new Response(JSON.stringify({ success: false, message: 'Ação não reconhecida' }), { status: 200, headers: corsHeaders });
  } catch (error) {
    console.error('[nome-funcao] Erro:', error);
    return new Response(JSON.stringify({ success: false, message: 'Erro interno. Tente novamente.' }), { status: 500, headers: corsHeaders });
  }
});

1.2 Nomenclatura de Clientes

VariávelFactoryQuando usarRLS
supabasecreateSupabaseClient(req)99% — operações autenticadasAtivo
supabasePubliccreateSupabasePublic()Lookup públicoAtivo
supabaseSystemcreateSupabaseSystem()Logging, auth, cronsBypass

PROIBIDO: supabaseAdmin, createClient inline, service_role no frontend.

1.3 Sanitização de Erros

  • NUNCA retornar error.message para o cliente
  • Usar mensagens contextuais por operação: "Não foi possível carregar os dados."
  • console.error para debug interno; mensagem genérica na Response
  • Catch global sempre retorna: "Erro interno. Tente novamente."

1.4 Padrão de Split

LinhasAção
< 800Arquivo único OK
800–1500Avaliar split
> 1500Split obrigatório

Padrão: index.ts como orquestrador fino (~150-350 linhas) + helpers em _shared/.

Regras do Split:

  1. Orquestrador mantém: roteamento (switch/if), estado in-memory (ex: burstTracker), EdgeRuntime.waitUntil, auth guard
  2. Helpers exportam funções puras que recebem dependências como parâmetro (injeção de dependência)
  3. Cada helper retorna Response diretamente — o orquestrador delega sem manipular o retorno
  4. Helpers NÃO importam entre si (evitar grafo de dependência circular)
  5. Constantes compartilhadas ficam em um helper dedicado (ex: portal-security.ts)

Splits realizados (Mar/2026):

Edge FunctionAntesDepois (orq.)Helpers
portal-escola3.929 linhas212 linhas6: portal-security, portal-login-aluno, portal-login-responsavel, portal-cadastro, portal-dashboard, portal-config
gestao-resultados3.137 linhas~190 linhas5: resultados-compute, resultados-queries, resultados-mutations, resultados-config, resultados-import
gestao-alunos2.413 linhas~170 linhas4: alunos-crud, alunos-validation, alunos-transfer, alunos-import

Catálogo completo dos helpers: docs/architecture/EDGE_FUNCTIONS_CATALOG.md → seção "Shared Helpers".

1.5 Logging

  • registrarLog() em todo CREATE / UPDATE / DELETE
  • req obrigatório (geolocalização via Cloudflare headers)
  • Ação: <modulo>.<operacao> lowercase
  • Diff: gerarAlteracoes(antes, depois) de _shared/diff-helper.ts

1.6 RLS Silent-Failure Check

Mutations com RLS ativo devem validar o resultado:

typescript
const { data, error } = await supabase.from('tabela').update({...}).eq('id', id).select().maybeSingle();
if (!data && !error) {
  return new Response(JSON.stringify({ success: false, message: 'Sem permissão.' }), { status: 403, headers: corsHeaders });
}

1.7 Batching de Requisições (batch_init)

Telas com 3+ requests ao mesmo backend DEVEM implementar action batch_init:

  • Backend usa Promise.all para paralelizar queries internas
  • Elimina overhead repetido de auth/feature-flag/CORS (~100-300ms por request)
  • Resposta única com todos os dados: { olimpiadas, stats, config }

Exemplos implementados:

TelaActionRequests consolidados
Resultadosbatch_init_resultados4 → 1
Muralbatch_init4 → 1

1.8 Resiliência a Erros Transitórios

  • invokeEdge faz retry automático (max 2) com backoff para 502/503/504
  • queryFn DEVE fazer throw em erro — nunca retornar estado vazio ([])
  • Retornar [] em erro impede retry do React Query e mascara falhas
  • Auth context diferencia timeout transitório (_transient) de sessão expirada
typescript
// ❌ PROIBIDO — engole erro, impede retry
queryFn: async () => {
  try { return await fetchData(); }
  catch { return []; }  // React Query acha que deu certo
}

// ✅ CORRETO — throw propaga para retry
queryFn: async () => {
  const result = await fetchData();
  if (!result.success) throw new Error(result.message);
  return result.data;
}

2. Frontend (Hooks)

2.1 React Query — Obrigatório

Critérios: listagem, dados compartilhados, paginação/filtros, CRUD, dados que crescem.

Exceções válidas (NÃO migrar):

  • Portal público (useMuralPortal) — sessão cookie-based
  • WebSocket realtime (useRealtimeNotificacoes)
  • Import stateful (useImportacaoSessao, useImportacaoResultados)

2.2 Template de Hook

typescript
const MODULO_KEYS = {
  all: ['modulo'] as const,
  list: (filtros: Filtros) => ['modulo', filtros] as const,
};

const CACHE_CONFIG = {
  staleTime: 5 * 60 * 1000,
  gcTime: 30 * 60 * 1000,
  refetchOnWindowFocus: false,
};

2.3 Tratamento de Erros

  • getUserFriendlyError(error) em todo onError de mutations
  • olpToast.error() para feedback visual
  • PROIBIDO: alert(), error.message direto no toast, import sonner direto

2.4 staleTime por Categoria

CategoriastaleTimeExemplo
Quase estático30minSéries, anos
Configurações10minPermissões
Operacional5minAlunos, turmas
Dashboards5minMétricas
Tempo real1minNotificações

2.5 Estabilidade de Hooks (useCallback) — 🔴 OBRIGATÓRIO

Toda função retornada por hook custom DEVE usar useCallback.

Funções sem useCallback criam nova referência a cada render. Quando usadas como dependências de useEffect, causam loop infinito de re-renders que congela a UI completamente.

Regras

  1. Dependências do useCallback devem ser estáveis: queryClient, mutation, refetchnunca dados reativos como items.length
  2. useEffect de mount redundante é proibido quando React Query já auto-carrega via useQuery
  3. Se um useEffect chama uma função que é essencialmente um refetch → remover o useEffect

Exemplo

typescript
// ❌ PROIBIDO — cria nova ref a cada render → loop infinito
const useMeuHook = () => {
  const listarDados = async () => { /* ... */ };
  return { listarDados };  // ref instável!
};

// ✅ CORRETO — ref estável entre renders
const useMeuHook = () => {
  const listarDados = useCallback(async () => { /* ... */ }, [queryClient]);
  return { listarDados };  // ref estável ✓
};

2.6 enabled — Sem Dependência Circular

enabled de um useQuery NUNCA deve depender de dado que só existe no resultado da própria query.

typescript
// ❌ DEADLOCK — anoEdicao só é setado quando query retorna, mas query não roda sem ele
useQuery({ enabled: anoEdicao !== null, queryFn: () => fetchDados(anoEdicao) });

// ✅ CORRETO — primeira chamada sem filtro, refinamento posterior
useQuery({ enabled: true, queryFn: () => fetchDados() });
// Depois, com queryKey diferente para filtro específico

3. Frontend (Componentes)

3.1 Feature Directory Pattern

Componentes > 400 linhas ou 3+ subcomponentes:

src/components/dominio/
  index.tsx          ← orquestrador (hooks, export único)
  helpers.ts         ← tipos + funções puras (sem React)
  dominio-*.tsx      ← subcomponentes com prefixo

3.2 Regras

  • Prefixo de domínio em subcomponentes
  • Hooks de dados no orquestrador (props estritamente tipadas para sub)
  • Separação helpers.ts para lógica pura testável

4. Segurança

4.1 Princípios

  • Zero Trust: Backend revalida tudo (nunca confiar no frontend)
  • Fail-Close: Erro = bloqueia (não permite acesso)
  • Sem SERVICE_ROLE no frontend
  • Sem createClient inline em Edge Functions
  • Rate limits em endpoints públicos (portal)

4.2 Autenticação

  • Cookie HttpOnly (olp_auth 8h, olp_mural 2h)
  • JWT customizado via extractAuthenticatedUser(req)
  • verify_jwt = false no config.toml (auth via cookies)

5. Testes

5.1 Três Camadas

CamadaFerramentaO que testa
UnidadeVitestLógica pura, visibilidade
IntegraçãoVitest + QueryClient realCiclo React Query, invalidação
ContratoDeno testsCORS, auth guard, response format
E2EPlaywrightJornadas cross-role

5.2 Padrão de Integração

  • integration-helpers.ts com QueryClient real
  • Mock apenas do invokeAction/invokeEdge (HTTP boundary)
  • Spy em invalidateQueries para validar cache wiring
  • vi.mock factory inline (não usar variáveis externas — hoisting)

6. Documentação

6.1 Regra Zero

Toda mudança significativa DEVE atualizar docs/ na mesma entrega.

6.2 Hierarquia

  • Knowledge (project settings): Sucinto, regras críticas, referências para docs/
  • docs/: Detalhado, com exemplos e templates
  • @audit: Checklist de validação contínua (16 seções, 20+ domínios)

1.9 Validação de Input com Zod (Roadmap)

Status: Planejado — implementação faseada.

Atualmente nenhuma Edge Function valida input com schema formal. O padrão futuro usará Zod via import Deno:

typescript
import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts';

const schema = z.object({
  nome: z.string().min(1).max(255),
  escola_id: z.string().uuid(),
});

const parsed = schema.safeParse(params);
if (!parsed.success) {
  return new Response(JSON.stringify({
    success: false,
    message: 'Dados inválidos',
  }), { status: 400, headers: corsHeaders });
}

Fases de implementação:

FaseEscopoEFs
1Criar _shared/validation.ts com helper validateParams()
2Auth, pagamentos, portal~8
3Demais EFs (gradual, por manutenção)~45

7. Hooks Migrados para React Query (Referência)

Todos os hooks abaixo foram migrados e estão em conformidade:

useAdminEscolas, useBannersLogin, useEventosCalendario, useTarefasEscola, useGestaoResultados, useImportacaoResultados, useMuralEscola, useHeadersNovidades, useCursosData, useTutoriaisData, useTemplatesData, useOlimpiadasData, useOlimpiadasCoordenador, useInscricoesOlimpiada, useVideosCoord, useComunicacaoEscola, useAdminDashboardMetrics, useAdminUsuariosEscola, useEscolaDados, useAnoLetivo, useTransferenciaAlunos, useEscolaPagamentos, useMuralConfig, useGestaoTurmas, useGestaoAlunos, useGestaoResponsaveis, useAdminLogs, useAdminUsuarios, useUserProfile, useTrialInfo, useNotificacoes, useAdminSmsLogs, useAdminAssinaturas, useAdminFaturas, useMercadoPago, useUsuariosEscola, useAdminCronMonitor


Referências