Skip to content

Code Style - Padrões de Código

Estrutura de módulos

OBRIGATÓRIO: Todo módulo em src/modules/<mod>/ tem exatamente estas subpastas:

src/modules/<mod>/
├── services/      # camada de API (getXxxService, postXxxService)
├── hooks/         # stores Pinia + composables (useXxx.ts)
├── components/    # componentes Vue do módulo
├── pages/         # páginas Vue do módulo
└── router/        # rotas do módulo

Regras inegociáveis

RegraDescrição
PROIBIDO stores/Pastas stores/ não existem. Stores Pinia vivem em hooks/useXxx.ts
PROIBIDO composables/Pastas composables/ não existem. Composables vivem em hooks/useXxx.ts
Hooks unificadosStores e composables compartilham a mesma pasta hooks/ e o mesmo padrão de nomenclatura useXxx.ts
Sem sufixo StoreNUNCA usar useXxxStore. O nome do hook é sempre useXxx, mesmo quando for defineStore
defineStore internoexport const useCompliance = defineStore('compliance', () => { ... }) o primeiro argumento do defineStore (o id) pode ter qualquer nome, mas o export é sempre useXxx
Split obrigatórioPROIBIDO referenciar store.x no template ou no corpo do script. Sempre destruturar. Segurar o instance uma única vez apenas para destruturar (const myStore = useXxx(); const { action } = myStore; const { ref } = storeToRefs(myStore)) é válido, desde que myStore não seja usado após isso. Padrão de referência: src/modules/contracts/components/Create/Flow/Review/ContactReview.vue
storeToRefs semprePara qualquer ref/computed de store Pinia, usar storeToRefs(). Ações são desestruturadas diretamente da store
Sem reactive({}) wrappingPROIBIDO retornar reactive({ ...storeState, ...actions }) em hooks. Devolver objeto plano preservando as refs

Exemplo correto

typescript
// ✅ CORRETO - src/modules/compliance/hooks/useCompliance.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCompliance = defineStore('compliance', () => {
  const items = ref<ComplianceFlow[]>([])
  const loading = ref(false)

  const activeItems = computed(() => items.value.filter((i) => i.status === 'active'))

  const loadItems = async () => { /* ... */ }

  return { items, loading, activeItems, loadItems }
})
vue
<!-- ✅ CORRETO - consumo em uma página -->
<script setup lang="ts">
  import { storeToRefs } from 'pinia'
  import { useCompliance } from '@/modules/compliance/hooks/useCompliance'

  const { items, loading, activeItems } = storeToRefs(useCompliance())

  const { loadItems } = useCompliance()

  loadItems()
</script>

Exemplo errado (tudo que a IA já fez e não pode voltar a fazer)

typescript
// ❌ ERRADO - sufixo Store, pasta stores/, sem split, sem storeToRefs
// src/modules/compliance/stores/compliance.ts
export const useComplianceStore = defineStore('compliance', () => { /* ... */ })

// consumo errado:
const store = useComplianceStore()
// template: store.items, store.loading, store.activeItems

// ❌ ERRADO - composable wrappando store e devolvendo reactive({})
export const useComplianceDetail = () => {
  const store = useComplianceDetailStore()

  return reactive({
    flow: store.flow,
    loading: store.loading,
    handleSave
  })
}

Antes de criar um hook novo

  1. grep pelo conceito no repositório inteiro: useXxx pode já existir em outro módulo
  2. Se existir, reutilizar. Nunca duplicar serviço ou hook de listagem (ex: providers)
  3. Ler project-structure.md e code-style.md antes de propor novo padrão

Uma função por arquivo de hook

OBRIGATÓRIO: Um arquivo de hook (useXxx.ts) pode conter apenas uma função no nível do módulo: o próprio hook. PROIBIDO declarar funções auxiliares (pure helpers, adapters, formatters, etc.) no escopo do módulo do mesmo arquivo.

Regras

RegraDescrição
Apenas o hook no móduloNenhuma const foo = (...) => ... ou function foo(...) no topo do arquivo além do export const useXxx = ...
Funções internas do hook são permitidasArrow functions atribuídas a const dentro do corpo do hook (actions, handlers, computeds) são parte do hook, não violam a regra
Constantes estáticas são permitidasconst FOO_MAP = {...}, const LIMIT = 42 no topo do arquivo são OK a restrição é sobre funções
Onde colocar helpers extraídosPure domain helpers → src/domain/<mod>/<nome>.ts. Helpers UI-only → src/modules/<mod>/components/<Componente>/helpers.ts ou arquivo sibling não-hook em hooks/ (sem prefixo use). Nunca no arquivo do hook

Motivação

Hook files são pontos de composição: misturar helpers pure com orquestração reativa dificulta a leitura, infla o arquivo e mascara violações de max-lines-per-function. Separar helpers em arquivos dedicados deixa o hook enxuto e os helpers testáveis isoladamente.

Exemplo

typescript
// ❌ ERRADO - helpers misturados no hook file
// src/modules/contracts/hooks/useContractCompliance.ts
const parseFlowList = <T>(raw: unknown): T[] => { ... }
const mergeCreatedFlows = (current, created) => { ... }
const buildInitialFlowMap = (providers, flows) => { ... }

export const useContractCompliance = defineStore('contractCompliance', () => {
  // ... usa os helpers acima
})

// ✅ CORRETO - helpers no domain, hook só compõe
// src/domain/contract/compliance.ts
export const parseFlowList = <T>(raw: unknown): T[] => { ... }
export const mergeCreatedFlows = (current, created) => { ... }
export const buildInitialFlowMap = (providers, flows) => { ... }

// src/modules/contracts/hooks/useContractCompliance.ts
import { parseFlowList, mergeCreatedFlows, buildInitialFlowMap } from '@/domain/contract/compliance'

export const useContractCompliance = defineStore('contractCompliance', () => {
  // usa os helpers importados
})

Hooks de módulo não recebem parâmetros

OBRIGATÓRIO: Hooks/composables de módulo (src/modules/<mod>/hooks/) não têm parâmetros. Todo estado vem do store; um elemento DOM entra por uma ação (setMeasurer(el), setOverlay(el)), nunca como argumento do hook. Padrões como useStructurePagination(refs), useVariableParser(content) ou usePlacementOverlay(overlayRef) são proibidos em hooks de módulo.

Exceção: componentes genéricos em src/components/

Componentes genéricos e reutilizáveis em src/components/ (ex: DocumentCanvas) são prop-driven e não têm store de módulo — e costumam ter múltiplas instâncias na mesma tela (preview + revisão + modal). Um store Pinia é singleton, então não serve (as instâncias colidiriam). Para esses casos, os composables co-localizados em src/components/<Componente>/podem receber as props como Ref:

typescript
// ✅ CORRETO - apenas para componentes genéricos prop-driven em src/components/
export const useDocumentPagination = (options: { html: Ref<string>; margins?: Ref<...> }) => { ... }

// no componente
const { isPaginated, pages } = useDocumentPagination({ html: toRef(props, 'html'), ... })

Regras que continuam valendo mesmo nessa exceção: um hook por arquivo (sem const/método/ type/interface entre imports e o export const useXxx — constantes vão pra dentro do corpo; tipos de parâmetro são inline na assinatura), sem tipar retorno, sem lifecycle no hook (expõe init/reset, o componente pluga), e o componente segue pura view. A exceção é restrita a src/components/ genéricos; hooks de módulo continuam sem parâmetros.


Inversão de dependências para hooks complexos

OBRIGATÓRIO: Quando um hook/store fica complexo demais (warnings de max-lines-per-function: função acima de 100 linhas: ou complexidade ciclomática alta), NÃO esconder a complexidade dentro de métodos auxiliares no mesmo arquivo. A resposta correta é inverter as dependências: quebrar o hook em múltiplos hooks menores, cada um com uma responsabilidade única.

Regras

RegraDescrição
Um hook, uma responsabilidadeSe o hook tem mais de ~8-10 ações ou mistura preocupações distintas (ex: estado + histórico + modal + compliance), separe em hooks independentes
Componentes consomem múltiplos hooksÉ válido (e desejado) um componente puxar refs/ações de dois ou três hooks diferentes isso é preferível a consumir um único hook inchado
Dependência unidirecionalHooks menores podem importar outros hooks (ex: useContractComplianceproviders de useActiveContract), mas sem ciclos. Quando um hook "mãe" precisa resetar seus filhos, ele chama reset() de cada um explicitamente
Estado compartilhado fica no hook mais genéricoDados que vários hooks precisam (ex: providers, toolbar) ficam no hook base; os hooks especializados leem via storeToRefs
Não esconda complexidade com wrappersNão criar um hook "facade" que apenas reexporta tudo dos hooks menores isso recria o problema. O componente importa diretamente os hooks que precisa

Quando aplicar

Gatilhos para inverter:

  • Lint warning max-lines-per-function no defineStore(() => { ... }) ou composable
  • Warning de complexidade ciclomática (complexity)
  • Mais de um "capítulo" lógico distinto no mesmo hook (ex: dados de listagem + estado de modal + fluxo de edição)
  • Funcs auxiliares com escopo interno que só existem por causa de um único fluxo

Exemplo

typescript
// ❌ ERRADO - um store gigante com múltiplas responsabilidades
export const useActiveContract = defineStore('activeContract', () => {
  // 40 linhas de estado do contrato
  // 60 linhas de histórico
  // 120 linhas de compliance modal
  // 80 linhas de transições
  // → warning max-lines-per-function (300 linhas)
})

// ✅ CORRETO - hooks especializados, consumidos em conjunto
// useActiveContract.ts  base (< 100 linhas)
export const useActiveContract = defineStore('activeContract', () => {
  const activeSidebarPanel = ref(...)
  const toolbar = computed(() => contractDetail.toolbar)
  // transições, navegação de painel, display helpers
})

// useContractHistory.ts  histórico isolado
export const useContractHistory = defineStore('contractHistory', () => {
  const historyItems = ref([])
  const loadHistory = () => { ... }
})

// useContractCompliance.ts  modal de compliance isolado
export const useContractCompliance = defineStore('contractCompliance', () => {
  const showAttachComplianceModal = ref(false)
  const { providers } = storeToRefs(useActiveContract())
  // form, pendingAssignments, handlers
})

// No componente
const { toolbar, statusType } = storeToRefs(useActiveContract())
const { historyItems } = storeToRefs(useContractHistory())
const { showAttachComplianceModal } = storeToRefs(useContractCompliance())

Lifecycle hooks só em páginas/componentes

OBRIGATÓRIO: onMounted, onBeforeUnmount, onUnmounted, onActivated, onDeactivated etc. NUNCA vivem dentro de useXxx.ts (hook ou store). Lifecycle pertence ao componente Vue (página ou componente) que tem ciclo de vida, o hook expõe init(), reset() (e similares) e o consumidor pluga no ciclo.

Regras

RegraDescrição
Hook não importa lifecycleNada de import { onMounted } from 'vue' em hooks/useXxx.ts. Se o hook precisa de side-effects no mount, encapsule em init()
Cleanup em reset()O cleanup que iria em onBeforeUnmount/onUnmounted mora numa action reset() retornada pelo hook
Página/componente pluga o cicloconst { init, reset } = useXxxPage(); onMounted(init); onBeforeUnmount(reset)
Vale para stores Pinia tambémdefineStore setup-style segue a mesma regra: expõe init/reset, sem onMounted no corpo
Watchers OK no hookwatch/watchEffect podem ficar no hook (não dependem do ciclo de vida do componente para setup)

Motivação

Hook é orientado a estado e ações; ciclo de vida pertence ao componente que o consome. Embutir onMounted no hook acopla cedo demais e em remontagens (HMR, navegação, troca de rota com <component :key>) o efeito pretendido duplica ou roda em horas erradas. Mantendo lifecycle só na página, o ciclo de vida fica coerente e previsível.

Exemplo

typescript
// ❌ ERRADO - lifecycle dentro do hook
// src/modules/contracts/hooks/useDetailContract.ts
export const useDetailContract = () => {
  // ...

  onMounted(bootstrap)
  onUnmounted(() => {
    activeContract.reset()
    compliance.reset()
  })

  return { openCancelModal }
}
typescript
// ✅ CORRETO - hook expõe init/reset, página pluga
// src/modules/contracts/hooks/useDetailContract.ts
export const useDetailContract = () => {
  // ...

  const init = () => {
    bootstrap()
  }

  const reset = () => {
    activeContract.reset()
    compliance.reset()
  }

  return { openCancelModal, init, reset }
}
vue
<!-- src/modules/contracts/pages/DetailContract.vue -->
<script setup lang="ts">
  import { onMounted, onBeforeUnmount } from 'vue'
  import { useDetailContract } from '@/modules/contracts/hooks/useDetailContract'

  const { init, reset } = useDetailContract()

  onMounted(init)
  onBeforeUnmount(reset)
</script>

Estrutura de Componentes Vue

OBRIGATÓRIO: Seguir a ordem <template>, <script setup>, <style>

vue
<template>
  <!-- Template HTML -->
</template>

<script setup lang="ts">
// Lógica do componente
</script>

<style lang="scss" scoped>
// Estilos SCSS
</style>

Regras de Código

RegraDescrição
Sem comentáriosNão adicionar comentários no código. O código deve ser autoexplicativo
Script SetupUsar sempre <script setup lang="ts">
TypeScriptTodo código deve ser tipado
Sem tipar retornoNÃO tipar retorno de funções. O TypeScript infere automaticamente
Sem if inlinePROIBIDO if em uma linha. Sempre usar bloco { } com quebra de linha
Sem return inlinePROIBIDO return na mesma linha do if. Sempre dentro do bloco { }
Linha vazia após ifSempre deixar uma linha vazia após o fechamento } de um bloco if
Linha vazia após const/letSempre deixar uma linha vazia após cada declaração const ou let
SCSSUsar <style lang="scss" scoped> para estilos
ComposablesExtrair lógica reutilizável em composables
Sem aninhamentoPROIBIDO aninhar if ou for. Usar early returns e métodos auxiliares
Responsabilidade únicaCada método deve ter apenas uma responsabilidade. Extrair lógica em funções menores
Sem lógica em componentesComponentes são pura view. Toda lógica de negócio, filtros, computeds e ações deve ficar em stores ou composables
Lifecycle só em páginas/componentesonMounted/onBeforeUnmount/onUnmounted etc. NUNCA dentro de hooks/useXxx.ts. Hook expõe init()/reset(); página chama onMounted(init) / onBeforeUnmount(reset)
Sem try/catch em componentesTratamento de erros deve ficar na store ou composable. Componentes apenas chamam métodos
storeToRefs + splitconst { refs } = storeToRefs(useXxx()) para variáveis reativas + const { actions } = useXxx() para ações. PROIBIDO const store = useXxx() seguido de store.xxx no template/script
i18n em hooks/componentesUsar const { t } = useI18n(). PROIBIDO i18n.global.t em hooks, stores e componentes. i18n.global.t é permitido apenas em arquivos de router/ (meta.title em lazy loading) e utils puros sem reatividade
Validação de formUsar rules do n-form por campo, com mensagens específicas para cada regra. PROIBIDO validar manualmente campos obrigatórios com message.error genérico. message.error é reservado para erros de backend no .catch
v-max-length obrigatórioTodo <n-input> e <n-textarea> que recebe texto livre DEVE usar a diretiva v-max-length="TEXT_LENGTH.*" (de @/domain/common/constants). PROIBIDO usar :maxlength HTML pode ser burlado via DevTools/paste. Search inputs usam TEXT_LENGTH.SEARCH. Detalhes em input-length-limits.md
Sem reactive({}) em hooksHooks/composables retornam objeto plano de refs e ações. PROIBIDO return reactive({ ... }) wrappando estado já reativo causa dupla indireção e quebra storeToRefs
BEM no SCSSUsar convenção BEM (block__element--modifier) com @apply Tailwind nos estilos SCSS
Sem cores hardcodedNão usar cores fixas (rgba, #hex). Usar classes Tailwind com variantes dark: ou variáveis CSS do tema para suportar tema claro/escuro e customização futura
Sem props/emits em nestedComponentes nested dentro do mesmo módulo devem acessar a store diretamente via storeToRefs() em vez de receber dados por props/emits. Props/emits são para componentes genéricos reutilizáveis entre módulos
router.push pelo namePROIBIDO usar router.push('/path') com path string. Sempre usar router.push({ name: 'route-name' }) pois o path pode mudar
Erros do backend no catchNo .catch() de chamadas de serviço, sempre exibir a mensagem vinda do backend: catch((e) => { message.error(e?.response?.data?.message) }). Não usar mensagens genéricas de i18n no catch, o backend já retorna mensagens assertivas

Separação de responsabilidades: componentes vs stores

Componentes são pura view

Componentes não devem conter lógica de negócio. Toda lógica deve estar em stores ou composables.

typescript
// ❌ ERRADO - lógica no componente
const search = ref('')
const debouncedSearch = ref('')
const updateDebounced = debounce((v: string) => { debouncedSearch.value = v }, 500)
watch(search, (v) => updateDebounced(v))
const filtered = computed(() => items.filter(...))
const handleDelete = (id: string) => { dialog.warning({ onPositiveClick: () => store.delete(id) }) }

// ✅ CORRETO - tudo na store, componente só consome
const store = useItemsStore()

const { search, filteredItems, loading } = storeToRefs(store)

const { init, handleDelete } = store

storeToRefs para variáveis, desestruturação para ações

typescript
// ❌ ERRADO - acessar tudo via store.xxx
const store = useStore()
// template: store.loading, store.items, store.deleteItem()

// ✅ CORRETO - separar refs de ações
const store = useStore()

const { items, loading, search } = storeToRefs(store)

const { deleteItem, loadItems, init } = store

Sem try/catch em componentes

typescript
// ❌ ERRADO - try/catch no componente
const handleSave = async () => {
  saving.value = true
  try {
    await api.save(data)
    message.success('Salvo')
  } catch {
    message.error('Erro')
  } finally {
    saving.value = false
  }
}

// ✅ CORRETO - componente só chama a store
const handleSave = async () => {
  await saveProfile()
}

Princípios de Clean Code

Sem if inline e sem return inline

typescript
// ❌ ERRADO - if inline com return
if (!user) return null
if (items.length === 0) return

// ✅ CORRETO - bloco com chaves e quebra de linha
if (!user) {
  return null
}

if (items.length === 0) {
  return
}

Linha vazia após if, const e let

typescript
// ❌ ERRADO - sem espaçamento
const user = getUser()
const name = user.name
if (!name) {
  return
}
const formatted = formatName(name)

// ✅ CORRETO - linha vazia após cada const/let e após cada bloco if
const user = getUser()

const name = user.name

if (!name) {
  return
}

const formatted = formatName(name)

Não tipar retorno de funções

typescript
// ❌ ERRADO - retorno tipado manualmente
const getUser = (): User => { ... }
const isValid = (value: string): boolean => { ... }

// ✅ CORRETO - TypeScript infere o retorno
const getUser = () => { ... }
const isValid = (value: string) => { ... }

PROIBIDO aninhamento de estruturas de controle

typescript
// ❌ ERRADO - if aninhado
const processItems = (items: Item[]) => {
  if (items.length > 0) {
    for (const item of items) {
      if (item.active) {
        // lógica
      }
    }
  }
}

// ✅ CORRETO - early return + método auxiliar
const processItems = (items: Item[]) => {
  if (items.length === 0) {
    return
  }

  const activeItems = items.filter(item => item.active)

  activeItems.forEach(processItem)
}

const processItem = (item: Item) => {
  // lógica isolada
}

Responsabilidade única por método

typescript
// ❌ ERRADO - múltiplas responsabilidades
const handleSubmit = async () => {
  const isValid = validateForm()
  if (!isValid) {
    showError('Formulário inválido')
    return
  }
  const data = transformData(formData)
  await api.save(data)
  showSuccess('Salvo!')
  router.push('/list')
}

// ✅ CORRETO - responsabilidades separadas
const handleSubmit = async () => {
  if (!validateForm()) {
    return showValidationError()
  }

  await saveData()
  navigateToList()
}

const showValidationError = () => showError('Formulário inválido')

const saveData = async () => {
  const data = transformData(formData)

  await api.save(data)
  showSuccess('Salvo!')
}

const navigateToList = () => router.push('/list')

Nomenclatura

TipoConvençãoExemplo
ComponentesPascalCaseContractList.vue
Hooks (store ou composable)camelCase com prefixo use, sem sufixo StoreuseContracts.ts exportando useContracts
ServicescamelCase com sufixo ServicegetContractsService
Types/InterfacesPascalCaseContract, ContractPart
Arquivos de páginaPascalCaseListContracts.vue

Padrão de Props e Emits

typescript
const props = defineProps<{
  currentStep: number
  title: string
}>()

const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'setStep', index: number): void
}>()

Padrão de Composables

typescript
export const useContracts = () => {
  const items = ref<Contract[]>([])
  const loading = ref(false)

  const loadContracts = async () => {
    loading.value = true
    const { data } = await getContractsService()
    items.value = data
    loading.value = false
  }

  return {
    items,
    loading,
    loadContracts
  }
}

Regras de i18n (Traduções)

RegraDescrição
Sentence caseUsar letra maiúscula apenas no início da frase e em nomes próprios. Nunca usar Title Case (maiúscula em todas as palavras)
Nomes própriosManter maiúsculas apenas em: siglas (CPF, CNPJ, FAQ), marcas (WhatsApp), títulos de documentos legais (Termos de Serviço, Política de Privacidade), categorias jurídicas (Pessoa Jurídica, Pessoa Física)

Sentence case em traduções

❌ ERRADO - Title Case
"Novo Contrato"
"Total de Horas"
"Relatório Mensal"
"Histórico de Contratos"

✅ CORRETO - Sentence case
"Novo contrato"
"Total de horas"
"Relatório mensal"
"Histórico de contratos"

✅ CORRETO - Exceções (nomes próprios, siglas, títulos legais)
"Termos de Serviço"
"Política de Privacidade"
"Pessoa Jurídica"
"CPF do responsável"

Limites de Caracteres em Inputs

OBRIGATÓRIO: Toda entrada de texto vinda do usuário (form, search, textarea) deve usar a diretiva v-max-length="TEXT_LENGTH.*". Front bloqueia digitação E intercepta paste/DOM-edit; backend rejeita payloads maiores via 400. Defesa em profundidade.

A diretiva v-max-length (registrada em src/main.ts) trunca o valor no estado, não só no atributo HTML. :maxlength puro é proibido porque pode ser burlado via DevTools.

A tabela canônica de tiers e o mapeamento campo → tier vive em input-length-limits.md. Os valores espelham o backend.

Em <n-input> / <n-textarea>

vue
<template>
  <n-input
    v-model:value="form.name"
    v-max-length="TEXT_LENGTH.SHORT"
    :placeholder="t('placeholder.name')"
  />
</template>

<script setup lang="ts">
import { TEXT_LENGTH } from '@/domain/common/constants'
</script>

Em search inputs de listagem (TableHeader.vue)

vue
<n-input
  v-model:value="search"
  v-max-length="TEXT_LENGTH.SEARCH"
  :placeholder="t('search.placeholder')"
/>

Em FormRules (validação de submit)

Para campos críticos, complemente o :maxlength com regra max no FormRules:

typescript
const rules: FormRules = {
  description: [
    { required: true, message: t('global.formErrorRequired') },
    { max: TEXT_LENGTH.XLONG, message: t('global.formErrorMaxLength', { max: TEXT_LENGTH.XLONG }) }
  ]
}

Em editores ricos (htmlContent)

:maxlength não funciona em Tiptap/Quill. Valide .length no hook antes de submeter:

typescript
const isHtmlValid = computed(() => htmlContent.value.length <= TEXT_LENGTH.HTML)

E desabilite o botão de salvar quando exceder o limite.


Checklist para Novos Componentes

  • [ ] Usar <script setup lang="ts">
  • [ ] Não adicionar comentários no código
  • [ ] Seguir ordem: template → script → style
  • [ ] Usar <style lang="scss" scoped>
  • [ ] Tipar todas as props e emits
  • [ ] Usar composables para lógica reutilizável
  • [ ] Seguir convenções de nomenclatura
  • [ ] Usar classes Tailwind para layout básico
  • [ ] Usar i18n para textos estáticos
  • [ ] Aplicar v-max-length="TEXT_LENGTH.*" em todo <n-input>/<n-textarea> de texto livre