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:
- Actions:
6-actions.md - Pages:
7-pages.md
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.ts→DatePicker - 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
Componentno nome da classe - Sufixo
Service,DirectiveePipesão obrigatórios no nome da classe e do arquivo (ex:sse-service.ts→SseService,tooltip-directive.ts→TooltipDirective,date-format-pipe.ts→DateFormatPipe) - Seletores Angular:
hg-*para@hg/ui,app-*para apps
Variáveis e funções
camelCasepara funções e variáveisSCREAMING_SNAKE_CASEpara constantessnake_casepara colunas de banco- Factory functions:
createXxx() - Dado vazio:
null(nunca'') — propriedades que podem não ter valor devem serT | nullouT?, 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+)
| Deprecated | Usar |
|---|---|
@Input() | input() / input.required() |
@Output() + EventEmitter | output() |
@ViewChild | viewChild() / viewChild.required() |
@ContentChild | contentChild() |
@ContentChildren | contentChildren() |
@HostBinding | host: { '[attr.x]': 'value()' } |
@HostListener | host: { '(event)': 'handler($event)' } |
constructor(private x: X) | x = inject(X) |
OnDestroy + ngOnDestroy | inject(DestroyRef).onDestroy() |
OnChanges + ngOnChanges | effect() |
subscribe() sem cleanup | .pipe(takeUntilDestroyed()) |
new EventEmitter() | output() |
CUSTOM_ELEMENTS_SCHEMA | Nã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
.htmlseparado (templateUrl) - Estilos: preferir Tailwind no template — não criar arquivos
.csssalvo exceções (ex: syntax highlight com::ng-deep) - Nunca
template:inline oustyles:inline - ESLint
no-inline-templateestá ativo comoerror
Host element
- Usar
host: { class: 'contents' }(Tailwind) — nuncastyle: '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/setIntervaldevem ser limpos viadestroyRef.onDestroy()- Nested subscriptions → usar
switchMapoumergeMap
Zoneless
- O app usa
provideZonelessChangeDetection()— nunca usarNgZone - Signals e
markForCheck()triggam change detection automaticamente
Slots (content projection)
- Usar atributo direto:
<div header>— nuncaslot="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(), ouparentElementtraversal - 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 remsempre — nuncapx- Inline
[style]somente para CSS custom properties (--color-*) - Componentes
hg-*usamdisplay: contentsviaclass: '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 sersnake_case
Resiliência
- Circuit breaker, retry, rate limiting: centralizado no
@hg/core - Nunca implementar resiliência diretamente nos services
ESLint
Regras custom ativas:
| Regra | Nível | O que faz |
|---|---|---|
one-class-per-file | error | Max 1 export class por arquivo + nome deve bater com filename |
no-inline-template | error | Proíbe template: e styles: inline — exige arquivos separados |
enforce-component-selector | error | Seletores devem seguir o padrão hg-* / app-* |
no-legacy-decorators | error | Proíbe @Input(), @Output(), @ViewChild(), @HostBinding(), etc. |
no-constructor-injection | error | Proíbe constructor(private x: X) — usar inject() |
no-component-suffix | error | Proíbe sufixo Component no nome da classe |
no-empty-string-default | warn | Proíbe inicializar com '' — usar null |
no-hg-component-classes | error | Proíbe class/style em elementos hg-* no template |
enforce-hg-props | error | Valida props em componentes hg-* contra tipos TS |
enforce-hg-props-kebab-case | warn | Props estáticas em hg-* devem ser kebab-case |
no-html-comments | warn | Proíbe comentários HTML em templates |
no-slot-attribute | error | Proíbe slot="..." — usar atributo direto |
no-px-units | warn | Proíbe px em estilos inline no template — usar rem |
no-empty | error | Proíbe blocos catch vazios (root) |
no-restricted-imports (NgZone) | error | Proíbe import de NgZone — app é zoneless |
no-restricted-imports (Schema) | error | Proíbe import de CUSTOM_ELEMENTS_SCHEMA |
no-restricted-imports (Emitter) | error | Proíbe import de EventEmitter — usar output() |
Geração de código
pnpm generategera código a partir dos YAMLs em{module}/schemas/- Nunca editar
_gen/ou.gen/— são regenerados - Consultar
pnpm generate:listantes de criar qualquer domínio - Os YAMLs são a fonte de verdade para modelo, campos, actions e endpoints