Pular para o conteúdo principal

Guidelines

Referência técnica das decisões de código, arquitetura e padrões comuns ao monorepo. Vale para qualquer módulo (cms, hub, adm, sys, libs). Guidelines específicos estão em:

Para padrões de frontend, consultar também:

Princípios

SOLID, DRY, KISS, YAGNI, Law of Demeter. Na prática:

  • Não criar abstrações para uso único
  • Não duplicar lógica — extrair para service ou utility
  • Não criar helpers "por precaução"
  • Componentes não devem conhecer a estrutura interna de objetos que não lhes pertencem
  • Cada classe/service tem uma única responsabilidade

Naming

Arquivos

  • kebab-case.ts — sem sufixos de tipo (.component.ts, .service.ts, .directive.ts)
  • Nome do arquivo = nome da classe em kebab-case: date-picker.tsDatePicker
  • Se houver colisão de nomes no barrel, renomear o arquivo (não a classe)

Classes

  • 1 classe exportada por arquivo (interfaces/types do mesmo domínio podem ficar no mesmo arquivo)
  • Nome da classe = PascalCase do nome do arquivo — sem exceção
  • Sem sufixo Component no nome da classe
  • Sufixo Service, Directive e Pipe são obrigatórios no nome da classe e do arquivo (ex: sse-service.tsSseService, tooltip-directive.tsTooltipDirective, date-format-pipe.tsDateFormatPipe)
  • Seletores Angular: hg-* para @hg/ui, app-* para apps

Variáveis e funções

  • camelCase para funções e variáveis
  • SCREAMING_SNAKE_CASE para constantes
  • snake_case para colunas de banco
  • Factory functions: createXxx()
  • Dado vazio: null (nunca '') — propriedades que podem não ter valor devem ser T | null ou T?, não inicializadas com string vazia

Angular

Padrão obrigatório

@Component({
selector: 'hg-example',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './example.html', // Nunca template inline
host: { class: 'contents' }, // Tailwind class, nunca style
})
export class Example {
// Props via signal API
label = input<string>();
items = input.required<Item[]>();
color = input('primary');

// State
loading = signal(false);

// Events
hgClick = output<void>();
hgChange = output<string>();

// Derivações
count = computed(() => this.items().length);

// DI via inject()
private service = inject(MyService);
private destroyRef = inject(DestroyRef);

// Queries
private contentRef = viewChild<ElementRef>('content');
private items = contentChildren(ItemComponent);
}

APIs obrigatórias (Angular 17+)

DeprecatedUsar
@Input()input() / input.required()
@Output() + EventEmitteroutput()
@ViewChildviewChild() / viewChild.required()
@ContentChildcontentChild()
@ContentChildrencontentChildren()
@HostBindinghost: { '[attr.x]': 'value()' }
@HostListenerhost: { '(event)': 'handler($event)' }
constructor(private x: X)x = inject(X)
OnDestroy + ngOnDestroyinject(DestroyRef).onDestroy()
OnChanges + ngOnChangeseffect()
subscribe() sem cleanup.pipe(takeUntilDestroyed())
new EventEmitter()output()
CUSTOM_ELEMENTS_SCHEMANão usar — todos os componentes são Angular

Exceção: OnDestroy é obrigatório em services providedIn: 'root' (DestroyRef não funciona neles).

Templates e estilos

  • Template: sempre em arquivo .html separado (templateUrl)
  • Estilos: preferir Tailwind no template — não criar arquivos .css salvo exceções (ex: syntax highlight com ::ng-deep)
  • Nunca template: inline ou styles: inline
  • ESLint no-inline-template está ativo como error

Host element

  • Usar host: { class: 'contents' } (Tailwind) — nunca style: 'display: contents'
  • Atributos dinâmicos: host: { '[attr.x]': 'signal()' }
  • Eventos: host: { '(click)': 'handler($event)' }

Subscriptions e lifecycle

  • takeUntilDestroyed(destroyRef) em toda subscription de stream infinito (SSE, route params, etc.)
  • Subscriptions HTTP (HttpClient) completam automaticamente — não precisam de cleanup
  • setTimeout/setInterval devem ser limpos via destroyRef.onDestroy()
  • Nested subscriptions → usar switchMap ou mergeMap

Zoneless

  • O app usa provideZonelessChangeDetection()nunca usar NgZone
  • Signals e markForCheck() triggam change detection automaticamente

Slots (content projection)

  • Usar atributo direto: <div header> — nunca slot="header"
  • <ng-content select="[header]"> matcha o atributo

Estrutura Frontend

Pages

Referência completa em 7-pages.md. Pages estendem Page de @hg/ui/page e declaram path, body (action component), modais filhos e sub-rotas.

Actions

Referência completa em 6-actions.md. Actions ficam em domains/{domain}/actions/{action-type}/. Tipos padrão: search, view, edit, list. Arquivo e template têm o mesmo nome da pasta. Seletor: app-domains-{domain}-action-{action}. Partials (componentes auxiliares não-action) ficam em domains/{domain}/partials/.

@hg/ui

Seletores

  • Componentes primitivos: hg-button, hg-card, hg-table
  • Componentes de action: hg-action-button, hg-action-form, hg-action-search, hg-action-view
  • Componentes de data: hg-data-table, hg-data-list, hg-data-board, hg-data-calendar
  • Componentes de page: hg-page-container, hg-page-modal
  • Apps: app-domains-*

Comunicação pai-filho

  • Filhos injetam o pai: inject(Table, { optional: true })
  • Nunca getAttribute(), closest(), ou parentElement traversal
  • Pais expõem estado via input() signals — filhos leem diretamente

Contagem de filhos dinâmicos

Padrão register/unregister em vez de MutationObserver:

// Pai
registerItem() { this.count.update(c => c + 1); }
unregisterItem() { this.count.update(c => c - 1); }

// Filho
constructor() {
this.parent?.registerItem();
this.destroyRef.onDestroy(() => this.parent?.unregisterItem());
}

Estilos no host

  • Sempre Tailwind class: host: { class: 'contents' }, host: { class: 'block min-w-0' }
  • Nunca style: no host
  • Nunca inline style="..." quando existe classe Tailwind equivalente

CSS e Tailwind

  • Tailwind v4 — diretiva @import "tailwindcss"
  • Usar utilitários Tailwind diretos (bg-primary-50, text-gray-700) — sem classes de contexto CSS
  • rem sempre — nunca px
  • Inline [style] somente para CSS custom properties (--color-*)
  • Componentes hg-* usam display: contents via class: 'contents'

Backend

Fastify

  • Erros tipados via errorHandler() — nunca try/catch vazio
  • Toda operação com banco em contexto de tenant: withTenant()
  • Queries cross-tenant: comentar // Cross-tenant system query
  • Respostas de API sempre em snake_case — tipos internos podem usar camelCase, mas ao montar o response (reply.send()) todas as chaves devem ser snake_case

Resiliência

  • Circuit breaker, retry, rate limiting: centralizado no @hg/core
  • Nunca implementar resiliência diretamente nos services

ESLint

Regras custom ativas:

RegraNívelO que faz
one-class-per-fileerrorMax 1 export class por arquivo + nome deve bater com filename
no-inline-templateerrorProíbe template: e styles: inline — exige arquivos separados
enforce-component-selectorerrorSeletores devem seguir o padrão hg-* / app-*
no-legacy-decoratorserrorProíbe @Input(), @Output(), @ViewChild(), @HostBinding(), etc.
no-constructor-injectionerrorProíbe constructor(private x: X) — usar inject()
no-component-suffixerrorProíbe sufixo Component no nome da classe
no-empty-string-defaultwarnProíbe inicializar com '' — usar null
no-hg-component-classeserrorProíbe class/style em elementos hg-* no template
enforce-hg-propserrorValida props em componentes hg-* contra tipos TS
enforce-hg-props-kebab-casewarnProps estáticas em hg-* devem ser kebab-case
no-html-commentswarnProíbe comentários HTML em templates
no-slot-attributeerrorProíbe slot="..." — usar atributo direto
no-px-unitswarnProíbe px em estilos inline no template — usar rem
no-emptyerrorProíbe blocos catch vazios (root)
no-restricted-imports (NgZone)errorProíbe import de NgZone — app é zoneless
no-restricted-imports (Schema)errorProíbe import de CUSTOM_ELEMENTS_SCHEMA
no-restricted-imports (Emitter)errorProíbe import de EventEmitter — usar output()

Geração de código

  • pnpm generate gera código a partir dos YAMLs em {module}/schemas/
  • Nunca editar _gen/ ou .gen/ — são regenerados
  • Consultar pnpm generate:list antes de criar qualquer domínio
  • Os YAMLs são a fonte de verdade para modelo, campos, actions e endpoints