# Diretrizes de desenvolvimento
## Sumário
[Design System](#design-system)
* [Standalone Components](#standalone-components)
* [Organização do Projeto](#organizacao-projeto)
* [Lazy-load nas rotas ](#lazy-loading)
* [Pré-carregamento de Dados com Resolvers](#resolvers)
[Padrões de Nomeclatura](#nomeclatura)
[Reatividade](#reatividade)
* [RxJS](#rxjs)
* [Gerenciamento de Estado e Subjects](#rxjs-gerenciamento-estado)
* [Encapsulamento e Compartilhamento de Subjects](#rxjs-compartilhamento-subjects)
* [Pureza e Efeitos Colaterais](#rxjs-efeitos-coleterais)
* [Gerenciamento de Subscriptions](#rxjs-gerenciamento-subscriptions)
* [Combinação de Múltiplos Observables](#rxjs-combinacao-observables)
* [Signals](#signals)
* [ReactiveForms](#reactive-forms)
* [Estrutura e Tipagem](#reactive-forms-estrutura)
* [Validação e Feedback ao Usuário](#creactive-forms-validacaos)
* [Reatividade](#reactive-forms-reativade)
[Componentização](#componentizacao)
* [CONTAINER (Smart Component)](#componentizacao-smart-component)
* [PRESENTER (Dumb Component - Feature Específica)](#componentizacao-dumb-component)
* [SHARED COMPONENT (Dumb Component - Reutilizável)](#componentizacao-shared-component)
[Comunicação entre Componentes](#comunicacao-components)
* [Comunicação Vertical](#comunicacao-components-vertical)
* [Comunicação Horizontal/Global](#comunicacao-components-horizontal)
* [Práticas a Evitar](#comunicacao-components-evitar)
* [Acesso Direto](#comunicacao-components-evitar-acesso-direto)
* [Anti-Performance e Impureza](#comunicacao-components-evitar-acesso-impureza)
[Estilização](#estilizacao)
* [Tema PrimeNG](#estilizacao-tema)
* [Estrutura](#estilizacao-tema-estrutura)
* [Palette](#estilizacao-tema-palette)
* [Spacing](#estilizacao-tema-spacing)
* [Typography](#estilizacao-tema-typography)
* [Theme](#estilizacao-tema-theme)
* [Components](#estilizacao-tema-components)
* [Index](#estilizacao-tema-index)
* [Tailwind CSS](#estilizacao-tailwind)
[Comunicação com Back-End](#comunicacao-back)
* [API Layer](#comunicacao-back-api-layer)
* [API Facade Layer](#comunicacao-back-api-facade-layer)
* [DTOs de Request](#comunicacao-back-dto-request)
* [DTOs de Response](#comunicacao-back-dto-response)
* [DTOs internos](#comunicacao-back-dto-interno)
[Tratamento de Exceções](#tratamento-excecoes)
[Testes](#teste)
* [Imports](#testes-imports)
* [Boas práticas](#testes-boas-praticas)
* [Organização e Nomeclatura](#testes-organizacao-nomeclatura )
[Melhores Práticas de Implementação](#melhores-praticas-de-implementacao)
[Glossário](#glossario)
___
## Design System<a name="design-system"></a>
### Standalone Components<a name="standalone-components"></a>
**Deve:**
- Criar componentes com `standalone: true` (Padrão na versão 19 do angular).
- Desde o Angular 17+, a abordagem standalone components permite eliminar a necessidade de NgModule, simplificando a estrutura e reduzindo acoplamento. Cada feature folder pode conter componentes standalone e importar diretamente dependências específicas, sem módulos intermediários.
- Importar dependências diretamente no `imports` do decorador `@Component`.
- O gerenciamento de dependências e provedores globais, incluindo as configurações de roteamento e serviços HTTP, deve ser centralizado no array providers do `app.config.ts` as funções auxiliares específicas do Angular, como `provideHttpClient` e `provideRouter`.
**Exemplo:**
```ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideAnimationsAsync(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
],
};
```
___
## Padrões de nomeclatura<a name="nomeclatura"></a>
* A idioma padrão para escrita de nomes de métodos, atributos, classes e arquivos é o inglês.
| Elemento | Case / Formato | Convenção e Padrão | Exemplo |
| :--- | :--- | :--- | :--- |
| **Arquivo** | `kebab-case` | `feature.type.ts` | `user-list.component.ts` |
| **Classe** | `PascalCase` | Sufixo correspondente ao tipo. | `UserListComponent` |
| **Interface** | `PascalCase` | Nome da entidade (com prefixo `I`). | `interface IUser { ... }` |
| **Enum** | `PascalCase` | Nome do conjunto de valores. | `enum StatusOrder { ... }` |
| **Atributo (Público)** | `camelCase` | Substantivo descritivo. | `username: string;` |
| **Atributo (Privado)** | `_camelCase` | Prefixo `_` uso interno. | `private _selectedItems: Item[];` |
| **Atributo (Observable)** | `camelCase$` | Sufixo `$` para indicar um stream. | `users$: Observable<Users[]>;` |
| **Constante de Classe** | `UPPER_SNAKE_CASE` | Para valores estáticos imutáveis. | `static readonly API_URL = '...';` |
| **Método (Público)** | `camelCase()` | Verbo ou frase verbal. | `getData()` |
| **Método (Privado)** | `_camelCase()` | Prefixo `_` para uso interno. | `private _validateForm()` |
| **Método (Handler)** | `on` + `NomeEvento` | Responde a eventos. | `onClick(event: Event)` |
| **Booleano** | `is/has/should` | Prefixo para clareza. | `isVisible: boolean;` |
___
### Organização do Projeto (Folder-by-Feature) ?<a name="organizacao-projeto"></a>
- Usar nomes individuais para arquivos individuais como `cidadao.service.ts`
- Plurais para agrupadores de multiplos itens com o `cidadaos/`
#### 📁 app/core
Contém todos os **recursos essenciais da aplicação**, carregados uma única vez no `bootstrap`.
**Inclui:**
- **services/** – serviços globais (autenticação, logger, configuração da API, etc.);
- **interceptors/** – interceptadores HTTP (ex: AuthInterceptor, LoadingInterceptor);
- **guards/** – rotas protegidas (`AuthGuard`, `RoleGuard`);
- **layout/** – componentes de layout global (navbar, sidebar, footer);
- **config/** – constantes e tokens de injeção global (`API_URL`, `ENVIRONMENT`).
> 🔹 O módulo configurado no `app.config.ts`.
#### 📁 app/features
Agrupa cada **módulo funcional** da aplicação de forma isolada.
Cada subpasta representa uma **feature independente**, com suas próprias **rotas, componentes, serviços e estado**.
**Exemplo: `/features/users`**
```
src/
├── app/
│ ├── core/ # Módulo central (singleton services e interceptors)
│ │ ├── guards/
│ │ │ ├── auth.guard.ts
│ │ │ ├── role.guard.ts
│ │ ├── interceptors/
│ │ │ ├── auth.interceptor.ts
│ │ │ ├── error-handler.interceptor.ts
│ │ ├── services/
│ │ │ ├── logging.service.ts
│ │ │ ├── auth.service.ts
│ │ ├── config/
│ │ │ ├── app.config.ts
│ │ └── core.providers.ts
│ │
│ ├── shared/ # Componentes, diretivas e pipes reutilizáveis
│ │ ├── components/
│ │ │ ├── button/
│ │ │ │ ├── button.component.ts
│ │ │ │ ├── button.component.html
│ │ │ │ └── button.component.scss
│ │ ├── pipes/
│ │ │ ├── cpf-mask.pipe.ts
│ │ ├── directives/
│ │ │ ├── autofocus.directive.ts
│ │ └── utils/
│ │ └── date.util.ts
│ │
│ ├── features/ # Áreas funcionais do sistema
│ │ ├── cidadaos/
│ │ │ ├── pages/
│ │ │ │ ├── cidadao-list/
│ │ │ │ │ ├── cidadao-list-table/
│ │ │ │ │ │ ├── cidadao-list-table.presenter.ts
│ │ │ │ │ │ ├── cidadao-list-table.presenter.html
│ │ │ │ │ │ └── cidadao-list-table.presenter.scss
│ │ │ │ │ ├── cidadao-list.container.ts
│ │ │ │ │ ├── cidadao-list.container.html
│ │ │ │ │ ├── cidadao-list.container.scss
│ │ │ │ │ └── cidadao-list.routes.ts
│ │ │ │
│ │ │ ├── services/
│ │ │ │ └── cidadao.service.ts
│ │ │ ├── models/
│ │ │ │ └── cidadao.model.ts
│ ├── app.routes.ts # Configuração das rotas standalone
│ ├── app.config.ts # Configuração global (bootstrapApplication)
│ └── main.ts
```
> 🔹 Facilita o **lazy loading** e o isolamento de responsabilidades.
> 🔹 Cada feature pode evoluir ou ser removida sem quebrar o restante da aplicação.
---
#### 📁 app/shared
Contém **elementos reutilizáveis** entre múltiplos módulos e recursos.
**Inclui:**
- **components/** – componentes genéricos (ex: `ModalComponent`, `LoadingSpinner`);
- **pipes/** – pipes de uso geral (`formatDate`, `maskCpf`);
- **directives/** – diretivas compartilhadas (`debounceClick`, `autoFocus`);
- **utils/** – helpers e funções utilitárias puras;
> 🔹 Nenhum componente aqui deve depender de um “feature” específico.
---
### Lazy-load nas rotas<a name="lazy-loading"></a>
- Carregar módulos da aplicação sob demanda (apenas quando o usuário acessa uma rota específica) e não no carregamento inicial. Isso reduz o tamanho do bundle inicial e melhora o tempo de carregamento da aplicação.
**Deve**:
- Utilizar **loadComponent** para carregar **Componentes** e **loadChildren** para carregar **Arquivos que definem as rotas de módulos**.
```ts
export const routes: Routes = [
{
path: Rotas.CLIENTES.listagem,
loadChildren: () => import('./features/clientes/cliente.routes').then((routes) => routes.default)
},
{
path: Rotas.LOGIN.login,
loadChildren: () => import('./features/login/login.routes').then((routes) => routes.default)
},
{
path: '',
redirectTo: Rotas.CLIENTES.listagem,
pathMatch: 'full',
},
];
```
### Diretrizes de Pré-carregamento de dados<a name="resolvers"></a>
**DEVE:**
- Utilizar Resolvers para **pré-carregar dados essenciais** para a renderização de um componente antes que a navegação para a rota seja concluída.
**NÃO DEVE:**
- Colocar lógica de negócio complexa ou chamadas excessivamente demoradas em um Resolver, pois isso pode bloquear a navegação e dar a impressão de que a aplicação está travada.
**Exemplo de Uso:**
- Criar a função Resolver:
```ts
export const produtoDetalheResolver: ResolveFn<ProdutoDto | null> = (route, state) => {
const produtoService = inject(ProdutoService);
const produtoId = route.paramMap.get('id');
if (!produtoId) {
return of(null);
}
return produtoService.getById(+produtoId).pipe(
catchError(error => {
console.error('Falha ao buscar produto no resolver:', error);
// Redirecionar para uma página de erro
return of(null);
})
);
};
```
- Aplicar o resolver na rota:
```ts
export const PRODUTOS_ROUTES: Routes = [
{
path: ':id',
component: ProdutoDetalheComponent,
resolve: {
// A chave 'produto' será usada para acessar o dado resolvido
produto: produtoDetalheResolver
}
}
];
```
- Acessar o dado no componente:
```ts
export class ProdutoDetalheComponent {
private _route = inject(ActivatedRoute);
// Acessando o dado resolvido de forma reativa
protected produto$: Observable<ProdutoDto | null> = this._route.data.pipe(
map(data => data['produto'])
);
// Acessando o dado resolvido de forma instantânea
protected produto: <ProdutoDto | null> = this._route.snapshot.data['produto']
}
```
---
## Diretrizes de Reatividade<a name="reatividade"></a>
### RxJS<a name="reatividade"></a>
O **RxJS** (Reactive Extensions for JavaScript) DEVE ser utilizado em cenários de **manipulação de streams de dados assíncronos** ou para a coordenação de eventos baseada em tempo.
**Nomenclatura:** `usuarioLogado$`
#### Gerenciamento de Estado e Subjects<a name="rxjs-gerenciamento-estado"></a>
| Elemento | DEVE | Contexto/Razão |**Exemplo de Uso Prático**|
| :--- | :--- | :--- | :--- |
| **Observable** | Utilizar para expor **streams de dados assíncronos** que são **somente leitura**. | <br>É o contrato fundamental de reatividade e promovem a imutabilidade para os consumidores. | Serviço que fornece uma lista de usuários obtida via `HttpClient.get()`.|
| **BehaviorSubject** | Utilizar para gerenciar estados que **precisam de um valor inicial**. | Essencial para estados globais, pois garante que novos assinantes recebam o estado atual imediatamente. | <br>State de autenticação que guarda o usuário logado.|
| **Subject** | Utilizar para eventos sem a necessidade de um estado inicial. | Ideal para eventos efêmeros (ex: cliques) onde apenas os assinantes ativos no momento da emissão devem receber o valor. | <br>Disparar evento quando um modal deve ser fechado. <br>|
**Exemplos:**
- Observable:
```ts
protected users$: Observable<Array<User>> = this.http.get<Array<User>>('/api/users')
```
- Subject:
```ts
const closeModal$ = new Subject<void>();
protected fecharModal(): void {
this.closeModal$.next();
}
```
- BehaviorSubject:
```ts
private userSubject = new BehaviorSubject<User | null>(null);
public login(user: User): void {
this.userSubject.next(user);
}
```
#### Encapsulamento e Compartilhamento de Subjects<a name="rxjs-compartilhamento-subjects"></a>
Para garantir o encapsulamento do estado (impedindo que componentes externos emitam valores):
**DEVE**
- Expor `Subject` e `BehaviorSubject` para o exterior **somente** após a conversão em um `Observable`, somente leitura.
* **Método:** Utilizar o operador **`asObservable()`**.
```ts
private _isLoading = new BehaviorSubject<boolean>(false);
public isLoading$: Observable<boolean> = this._isLoading.asObservable();
```
- Considerar a conversão de *Observables* que representam estado persistente para **Signals** utilizando **`toSignal()`**.
```ts
protected loggedUser = toSignal(this.authService.loggedUser$, { initialValue: null });
```
#### Pureza e Efeitos Colaterais<a name="rxjs-efeitos-coleterais"></a>
**DEVE**
- Utilizar o operador **`tap()`** exclusivamente para realizar efeitos colaterais (*side-effects*), como logs, debug ou chamadas não reativas, sem modificar o *stream* principal de dados.
```ts
public login(email: string, password: string): Observable<LoginResponse> {
return this.http.post<LoginResponse>(this.API, { email, password }).pipe(
tap({
next: (response) => {
localStorage.setItem('auth_token', response.token);
console.log('Token armazenado com sucesso!');
},
error: (err) => console.error('Falha no login:', err)
})
);
}
```
#### Gerenciamento de Subscriptions (Cancelamento)<a name="rxjs-gerenciamento-subscriptions"></a>
- **SEMPRE** cancelar a inscrição (unsubscribe) de **Observables** que não completam naturalmente, a fim de prevenir **vazamentos de memória**.
| Local de Uso | Método Preferencial | Detalhe |
| :--- | :--- | :--- |
| **Template (HTML)** | **AsyncPipe ($\text{Pipe Assíncrono}$)** | **MÉTODO PREFERENCIAL.** Lida automaticamente com a inscrição e o cancelamento no ciclo de vida do componente.
| **Componentes/Serviços** | **`toSignal()`** | Cria um Signal reativo a partir de um Observable. <br>A **subscription é limpa automaticamente** quando o componente é destruído.<br>| |
| **Componentes/Serviços** | Operadores do RxJS: **`takeUntil(destroy$)`** | Ideal para cancelar a inscrição na destruição do componente. <br>Permite cancelar várias inscrições ao mesmo tempo <br>|
| **Componentes/Serviços** | Operadores do RxJS: **`take(N)`** | Ideal para chamadas HTTP ou outras operações que completam após um número (N) de emissões. |
**Componentes/Serviços** | Operadores do RxJS: **`takeUntilDestroyed(destroyRef)`** | **Método moderno para Angular >=16.** Cancela automaticamente a inscrição quando o componente ou diretiva é destruído, sem necessidade de criar um `Subject` manualmente.
**Exemplos:**
* **AsyncPipe:**
```ts
//ts
public loading$: Observable<boolean> = this._loadingService.loading$;
//html
@if(loading$ | async){
<p-progress-spinner ariaLabel="loading" />
}
```
* **toSignal():**
```ts
//ts
protected weather = toSignal(this.weatherService.getWeather(), { initialValue: 'Carregando...' });
//html
{{ weather() }}
```
* **takeUntil(destroy$):**
```ts
export class TimerComponent implements OnDestroy {
private _destroy$ = new Subject<void>();
private _counter = 0;
constructor() {
interval(1000)
.pipe(
tap(() => console.log('tick')),
takeUntil(this._destroy$)
)
.subscribe((n) => (this._counter = n));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
```
* **take(N):**
```ts
public buscarUsuarios(): Observable<Array<User>> {
return this.http.get<User[]>('/api/users').pipe(take(1))
}
```
* **takeUntilDestroyed(destroyRef):**
```ts
import { destroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
const destroyRef = inject(DestroyRef);
observable$.pipe(takeUntilDestroyed(destroyRef)).subscribe(value => console.log(value));
```
#### Combinação de Múltiplos Observables<a name="rxjs-combinacao-observables"></a>
**DEVE:**
- Utilizar operadores de combinação como `forkJoin`, `combineLatest` ou `zip` para orquestrar a execução de múltiplos Observables de forma declarativa.
**NÃO DEVE:**
- Aninhar subscribes (callback hell).
| Operador | Caso de Uso Principal | Comportamento da Emissão |
| :--- | :--- | :--- |
| **forkJoin** | Executar **requisições HTTP paralelas e independentes** onde você precisa aguardar a conclusão de **todas** elas para prosseguir. | Emite um único valor (um array com o último valor de cada stream) somente **quando todos** os Observables de entrada completam. Se algum falhar, o `forkJoin` falha imediatamente.
| **combineLatest** | Cenários de reatividade onde você precisa de um valor combinado sempre que **qualquer um** dos Observables de entrada emitir um novo valor (após todos terem emitido pelo menos uma vez). | Emite um valor combinado (array) sempre que um dos Observables emite.| |
| **zip** | Quando é necessário combinar emissões de múltiplos Observables em pares, na ordem em que foram emitidas. | Emite um valor combinado (array) apenas quando cada Observable de entrada emitiu um valor no mesmo índice.|
**Exemplos:**
- **forkJoin:**
```ts
export class DashboardComponent implements OnInit {
private usuarioService = inject(UsuarioService);
private permissoesService = inject(PermissoesService);
private produtosService = inject(ProdutosService);
protected dadosTela = toSignal(
this.carregarDadosIniciais(),
{ initialValue: { usuario: null, permissoes: [], produtos: [] } }
);
private carregarDadosIniciais(): Observable<{ usuario: any; permissoes: any[]; produtos: any[] }> {
return forkJoin({
usuario: this.usuarioService.getUsuarioLogado(),
permissoes: this.permissoesService.getPermissoes(),
produtos: this.produtosService.getProdutosDestaque()
}).pipe(
map(({ usuario, permissoes, produtos }) => {
// A lógica só executa quando todas as 3 chamadas retornarem com sucesso
console.log('Todos os dados foram carregados com sucesso!');
return { usuario, permissoes, produtos };
}),
catchError(error => {
console.error('Ocorreu um erro ao carregar dados iniciais:', error);
return of({ usuario: null, permissoes: [], produtos: [] });
})
);
}
}
```
- **combineLatest:**
```ts
const precos = {
'Notebook': 5000,
'Mouse': 150,
'Teclado': 300
};
const taxasCambio = {
'BRL': 1,
'USD': 0.18,
'EUR': 0.17
};
const produtoSelecionado$ = new BehaviorSubject<string>('Notebook');
const moedaSelecionada$ = new BehaviorSubject<string>('BRL');
const precoFinal$ = combineLatest([
produtoSelecionado$,
moedaSelecionada$
]).pipe(
map(([produto, moeda]) => {
const precoBase = precos[produto];
const taxa = taxasCambio[moeda];
const precoConvertido = (precoBase * taxa).toFixed(2);
console.log(`Calculando: Produto=${produto}, Moeda=${moeda}`);
return `${moeda} ${precoConvertido}`;
})
);
```
- **zip:**
```ts
const cliques$ = fromEvent(document, 'click');
const teclasPressionadas$ = fromEvent(document, 'keydown').pipe(
map((event: KeyboardEvent) => event.key) // Extrai apenas o nome da tecla
);
const combinacaoCliqueTecla$ = zip(
cliques$,
teclasPressionadas$
);
combinacaoCliqueTecla$.subscribe(([eventoClique, nomeTecla]) => {
console.log(`Clique registrado enquanto a tecla "${nomeTecla}" havia sido a última pressionada.`);
});
```
___
### Signals<a name="signals"></a>
- Os **Signals** são primitivas de reatividade simples e síncronas que garantem a propagação de mudanças de maneira eficiente e rastreável.
**DEVE:**
- Ser a **opção preferencial** para estados reativos **locais** de um componente e para **leituras síncronas** no *template*.
- Utilizar a função `signal()` para gerenciar o estado mutável principal.
- Utilizar **`computed()`** para derivar valores de outros *Signals* de forma **declarativa e memorizada** (só recalcula quando as dependências mudam).
- Utilizar a função **`effect()`** exclusivamente para lidar com **efeitos colaterais** (side-effects) que não podem ser expressos no fluxo de dados.
**Exemplos:**
```ts
//Singal
protected count: Signal<number> = signal(0);
protected increment(): void {
this.count.update((value) => value + 1);
}
protected reset(): void {
this.count.set(0);
}
// Computed
protected unitPrice = signal(50);
protected quantity = signal(1);
// Valor derivado (recalcula apenas quando quantity ou unitPrice mudam)
protected total = computed(() => this.unitPrice() * this.quantity());
public increase(): void {
this.quantity.update((q) => q + 1);
}
public decrease(): void {
this.quantity.update((q) => Math.max(1, q - 1));
}
//effect
protected theme = signal(localStorage.getItem('theme') || 'light');
constructor() {
// Side-effect: salvar no localStorage quando o theme muda
effect(() => {
const current = this.theme();
localStorage.setItem('theme', current);
console.log(`Tema atualizado para: ${current}`);
});
}
toggleTheme() {
this.theme.update((t) => (t === 'light' ? 'dark' : 'light'));
}
```
___
### ReactiveForms (FormBuilder, FormGroup e Validators)<a name="reactive-forms"></a>
Os Formulários Reativos (Reactive Forms) **DEVEM** ser a escolha padrão para a gestão de dados complexos, validações e reatividade em formulários.
#### Estrutura e Tipagem<a name="design-system"></a>
* **DEVE:**
- Utilizar o **`FormBuilder`** (ou `NonNullableFormBuilder`) para construir **`FormGroup`**, **`FormControl`** e **`FormArray`**, garantindo concisão e legibilidade.
```ts
export class UsuarioFormComponent {
formUsuario!: FormGroup;
private _fb = inject(NonNullableFormBuilder);
constructor() {
this.buildForm();
}
private buildForm(): void {
this.formUsuario = this._fb.group<IFormUsuario>({
nome: this._fb.control('', { validators: [Validators.required] }),
email: this._fb.control('', { validators: [Validators.required, Validators.pattern('[0-9]{10}')]}),
age: this._fb.control(0, { validators: [Validators.required, Validators.min(20),Validators.max(50)]}),
});
}
public salvar(): void {
const formClienteValue: IFormUser = this.formCliente.getRawValue();
}
}
```
- Tipar o `FormGroup` utilizando uma **Interface** ou **Type** que reflita a estrutura do formulário. Isso garante segurança de tipo ao acessar e manipular os valores, com a criação do type ou interface seguindo o padrão de nomeclatura abaixo:
**Nomenclatura:** `IFormUsuario` ou `FormUsuario`
```ts
interface IFormUsuario {
nome: string;
email: string;
age: number;
}
```
- A lógica de construção do `FormGroup` deve ser encapsulada em um **método privado** do componente (ex: `this.buildForm()`) ou isolada em um **serviço dedicado** para formulários complexos.
- Utilizar **`FormArray`** para a gestão de listas dinâmicas e repetíveis de controles ou grupos de controles.
#### Validação e Feedback ao Usuário<a name="design-system"></a>
**DEVE:**
- Criar **Validadores customizados** quando a lógica de validação de um campo não puder ser atendida pelos validadores padrão disponibilizados pelo framework.
```ts
import { AbstractControl, ValidationErrors } from "@angular/forms";
export const dataNascimento = (controle: AbstractControl): ValidationErrors | null => {
let dataNascimentoFutura = 'A data de nascimento não pode ser futura.';
let dataNascimentoAnteriorA1900 = 'Não deve ser anterior a 1900.';
const dataNascimento = controle.value;
const erros: ValidationErrors = {};
if (!dataNascimento) {
return null;
}
const hoje = new Date();
if (dataNascimento > hoje) {
erros['dataInvalidaFuturo'] = dataNascimentoFutura;
}
const ano1900 = new Date(1900, 0, 1);
if (dataNascimento < ano1900) {
erros['dataNascimentoAnteriorA1900'] = dataNascimentoAnteriorA1900;
}
return Object.keys(erros).length ? erros : null;
}
```
- Utilizar **Validadores Assíncronos** (Async Validators) para verificações que dependem de operações assíncronas (ex: checagem de nome de usuário via API).
```ts
import { inject } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from "@angular/forms";
import { Observable, of } from "rxjs";
import { debounceTime, switchMap, map, catchError, first } from "rxjs/operators";
import { CpfService } from "../services/cpf.service";
export const cpfJaExisteValidator: AsyncValidatorFn = (
controle: AbstractControl
): Observable<ValidationErrors | null> => {
const cpfService = inject(CpfService);
if (!controle.value || controle.invalid) {
return of(null);
}
return controle.valueChanges.pipe(
debounceTime(500),
first(),
switchMap(cpf =>
cpfService.verificarCpf(cpf).pipe(
map(response => {
return response.emUso
? { cpfJaExiste: 'Este CPF já está cadastrado.' }
: null;
}),
catchError(() => of(null))
)
)
);
};
```
#### Reatividade<a name="design-system"></a>
**DEVE:**
- Utilizar **`valueChanges`** ou **`statusChanges`** em um `FormControl` para implementar lógica reativa complexa (ex: habilitar ou desabilitar campos, ou alterar a lista de validadores, com base no valor de outro campo).
> 💡 **Importante:**
> Sempre utilize uma estratégia para **encerrar a inscrição (`unsubscribe`)** das observables de `valueChanges` e `statusChanges`, pois essas streams não se completam automaticamente. Recomenda-se o uso de **`DestroyRef`** com o operador **`takeUntilDestroyed()`** — que realiza o unsubscribe automaticamente quando o componente é destruído, evitando **vazamentos de memória**.
```ts
export class UsuarioFormComponent {
private _fb = inject(FormBuilder);
protect formUsuario!: FormGroup;
private _destroyRef = inject(DestroyRef);
constructor() {
this.buildForm();
}
private buildForm(): void {
this.formUsuario = this._fb.group({
nome: this._fb.control('', { validators: [Validators.required] }),
doc: this._fb.control(CPF,{ validators: [Validators.required] } )
email: this._fb.control('', { validators: [Validators.required, Validators.pattern('[0-9]{10}')]}),
age: this._fb.control(0, { validators: [Validators.required, Validators.min(20),Validators.max(50)]}),
});
}
}
private criarEventosFormulario(): void {
this.form.controls.tipo.valueChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe((docType) => {
const doc = this.form.controls.doc;
if (tipo === 'CPF') {
doc.addValidators(cpfValidator);
} else {
doc.clearValidators();
}
doc.updateValueAndValidity();
});
}
}
```
---
## Diretrizes de Componentização<a name="componentizacao"></a>
- Container/Presenter/Component Pattern
### CONTAINER (Smart Component)<a name="componentizacao-smart-component"></a>
**Nomenclatura:** `cadastro-inscricao-divida.container.ts`
**O que DEVE ter:**
- Injeção de Services (DividaService, DevedorService, etc)
- Lógica de negócio e validações complexas
- Chamadas HTTP (via services)
- Gerenciamento de estado (RxJS, NgRx, Signals)
- Navegação entre rotas
- Manipulação de formulários complexos
**O que NÃO deve ter:**
- Estilização complexa (delegar para presenters)
- Lógica de apresentação pura
- Conhecimento de estrutura HTML detalhada
**Exemplo:**
```ts
export class DividasContainer implements OnInit {
private dividaService = inject(DividaService);
private devedorService = inject(DevedorService);
private router = inject(Router);
private refresh$ = new BehaviorSubject<void>(undefined);
private isLoading$ = new BehaviorSubject<boolean>(false);
dividas$: Observable<DividaDto[]>;
devedores$: Observable<DevedorDto[]>;
ngOnInit(): void {
this.devedores$ = this.devedorService.getTodos();
this.dividas$ = this.refresh$.pipe(
tap(() => this.isLoading$.next(true)),
switchMap(() => this.dividaService.getTodas()),
tap(() => this.isLoading$.next(false))
);
}
onSalvar(form: FormGroup): void {
this.isLoading$.next(true);
this.dividaService.criar(form.value).subscribe({
next: () => {
alert('Dívida salva com sucesso!');
form.reset();
this.refresh$.next();
},
error: (err) => {
alert('Erro ao salvar dívida.');
console.error(err);
this.isLoading$.next(false);
}
});
}
onEditar(divida: DividaDto): void {
this.router.navigate(['/dividas/editar', divida.id]);
}
onExcluir(id: number): void {
if (confirm('Tem certeza que deseja excluir esta dívida?')) {
this.isLoading$.next(true);
this.dividaService.excluir(id).subscribe(() => {
alert('Dívida excluída com sucesso!');
this.refresh$.next();
});
}
}
}
```
### PRESENTER (Dumb Component - Feature Específica)<a name="componentizacao-dumb-component"></a>
**Nomenclatura:** `cadastro-inscricao-divida-list.presenter.ts`
**O que DEVE ter:**
- Decoradores @Input() para receber dados
- Decoradores @Output() para emitir eventos
- Lógica de apresentação simples (formatação, condicionais visuais)
- Validações de UI (não de negócio)
- Responsividade e acessibilidade
**O que NÃO deve ter:**
- Injeção de Services de negócio
- Chamadas HTTP diretas
- Lógica de negócio complexa
- Navegação entre rotas
- Manipulação de estado global
**Exemplo:**
```ts
export class GerenciarDividasPresenter implements OnInit {
@Input() dividas: DividaDto[] | null = [];
@Input() devedores: DevedorDto[] | null = [];
@Input() isLoading = false;
@Output() salvarDivida = new EventEmitter<FormGroup>();
@Output() editarDivida = new EventEmitter<DividaDto>();
@Output() excluirDivida = new EventEmitter<number>();
private fb = inject(FormBuilder)
formDivida: FormGroup;
colunasTabela: ColunaTabela[] = [
{ chave: 'id', rotulo: '#' },
{ chave: 'nomeDevedor', rotulo: 'Devedor' },
{ chave: 'valor', rotulo: 'Valor (R$)' }
];
ngOnInit(): void {
this.formDivida = this.fb.group({
devedorId: [null, [Validators.required]],
valor: [null, [Validators.required, Validators.min(0.01)]],
descricao: ['']
});
}
...
}
```
```html
<form [formGroup]="formDivida" (ngSubmit)="onSubmit()">
<h2>Cadastrar Nova Dívida</h2>
<select formControlName="devedorId">
<option [ngValue]="null" disabled>Selecione o devedor</option>
<option *ngFor="let devedor of devedores" [value]="devedor.id">
{{ devedor.nome }}
</option>
</select>
<input type="number" formControlName="valor" placeholder="Valor da Dívida">
<button type="submit" [disabled]="isLoading">
{{ isLoading ? 'Salvando...' : 'Salvar Dívida' }}
</button>
</form>
<hr>
<h2>Dívidas Cadastradas</h2>
<div *ngIf="isLoading" class="spinner">Carregando...</div>
```
### SHARED COMPONENT (Dumb Component - Reutilizável)<a name="componentizacao-shared-component"></a>
**Nomenclatura:** `tabela-dados.component.ts`
Componentes genéricos que podem ser usados em múltiplas features do projeto.
**O que DEVE ter:**
- Decoradores @Input() para receber dados
- Decoradores @Output() para emitir eventos
- Altamente configurável e genérico
- NÃO conhece DTOs ou domínio de negócio
- Lógica de apresentação agnóstica de contexto
- Validações de UI genéricas
- Responsividade e acessibilidade
- Documentação clara de uso (JSDoc/Storybook)
**O que NÃO deve ter:**
- Injeção de Services de negócio
- Chamadas HTTP diretas
- Lógica de negócio complexa
- Navegação entre rotas
- Manipulação de estado global
- Conhecimento de DTOs específicos (use tipos genéricos)
- Dependência de features específicas
```ts
export class BotaoPrimarioComponent {
@Input() texto: string = 'Clique-me';
@Input() tipo: 'button' | 'submit' | 'reset' = 'button';
@Input() carregando: boolean = false;
@Input() desabilitado: boolean = false;
@Output() clique = new EventEmitter<void>();
onClick(): void {
if (!this.carregando && !this.desabilitado) {
this.clique.emit();
}
}
}
```
```html
<button
[type]="tipo"
[disabled]="carregando || desabilitado"
(click)="onClick()"
class="botao-primario">
<span *ngIf="carregando" class="spinner"></span>
<span *ngIf="!carregando">{{ texto }}</span>
</button>
```
---
## Diretrizes de Comunicação entre Componentes<a name="comunicacao-components"></a>
A comunicação entre componentes **DEVE** ser clara, reativa e seguir o princípio do **fluxo de dados unidirecional** sempre que possível, garantindo baixo acoplamento e alta manutenibilidade.
### Comunicação Vertical (Pai $\leftrightarrow$ Filho)<a name="comunicacao-components-vertical"></a>
| Fluxo | Mecanismo DEVE | Detalhes e Boas Práticas |
| :--- | :--- | :--- |
| **Pai → Filho** | **Inputs** (`@Input()` ou `input()`) | **PREFERIR** a função `input()` (baseada em **Signals**), que é o padrão moderno do Angular. Utilizar para passar **dados simples** ou **configurações** do componente pai para o filho. |
| **Filho → Pai** | **Outputs** (`@Output()` $\rightarrow$ `EventEmitter`) | O `Output` DEVE ser utilizado para **emitir eventos** quando uma ação ocorre no filho. Emitir **dados tipados e brutos**, evitando lógica de negócios complexa. |
**Exemplo:**
* Pai → Filho:
```ts
//TS componente pai
export class ListagemClienteComponent {
public clientes: WritableSignal<Array<ClienteListagemDto>> = signal([]);
...
}
//html componente pai
//Propriedade de input
<tabela-cliente [clientes]="clientes()" />
// TS componente filho
export class TabelaClienteComponent {
public clientes = input<Array<ClienteListagemDto>>();
...
}
```
* Filho → Pai:
```ts
export class FiltrosClienteComponent{
public onPesquisar = output<IFormCliente>();
protected form!: FormGroup<IFormCliente>;
public pesquisar(): void {
const formValue = this.form.getRawValue();
this.onPesquisar.emit(formValue);
})
}
...
}
//html componente pai
<filtros-cliente (onPesquisar)="pesquisar($event)" />
//TS componente pai
...
public pesquisar(fitros: IFormCliente): void {
this._apiService.pesquisar(fitros).pipe(take(1)).subscribe((response ) => {...});
}
...
```
### Comunicação Horizontal (Mesmo Nível e Global)<a name="comunicacao-components-horizontal"></a>
* **Serviço de Estado:** **DEVE** ser utilizado um **Serviço de Estado** injetável (`StateService`) quando os componentes não têm uma relação direta de Pai/Filho.
* **Reatividade:** O `StateService` DEVE implementar a reatividade utilizando:
* **BehaviorSubject** (RxJS) para armazenar o estado global de forma reativa.
* **Signals** para expor e gerenciar o estado reativo compartilhado.
* **Serviços de API:** **DEVE**-se usar serviços injetáveis para toda comunicação com APIs externas, isolando o componente da lógica de *fetch* e manipulação de dados brutos.
```ts
export class UsuarioLogadoService {
private _usuarioLogado: BehaviorSubject<UsuarioLogadoDto> =
new BehaviorSubject<UsuarioLogadoDto>(new UsuarioLogadoDto({}));
public adicionarUsuarioLogado(usuarioLogado: UsuarioLogadoDto): void {
this._usuarioPreLogado.next(usuarioLogado);
}
public get usuarioLogado$(): Observable<UsuarioLogadoDto> {
return this._usuarioLogado.asObservable();
}
}
```
### Práticas a Evitar (Antipatterns)<a name="comunicacao-components-evitar"></a>
#### Excesso de camadas de interação
* Evite criar longas cadeias de comunicação vertical (@Input e @Output) através de múltiplos níveis hierárquicos.
* Quando houver mais de um nível de distância entre o componente que origina a ação e o que a consome, DEVE ser utilizado um Serviço de Estado (State Service) para desacoplá-los.
**Exemplo**:
`pagina-perfil (Avô)` → `detalhes-usuario` (Pai) → `card-endereco-usuario` (Filho)
```html
<app-avô (evento)="aoOcorrer($event)">
<app-pai (evento)="aoOcorrer($event)">
<app-filho (evento)="aoOcorrer($event)">
...
</app-filho>
</app-pai>
</app-avô>
```
* O componente `card-endereco-usuario` possui o método excluir endereço, que cria um evento repassado para `detalhes-usuario` e depois para`pagina-perfil (Avô)`
#### Emissão de Objetos Complexos sem Tipagem
* Emitir eventos com objetos genéricos (any) ou estruturas complexas não tipadas, causa perda de segurança de tipo e quebra fácil após refatorações.
```ts
@Output() usuarioAlterado = new EventEmitter<any>();
this.usuarioAlterado.emit({ nome: 'Teste', idade: 30 });
```
#### Acesso Direto (Alto Acoplamento)<a name="comunicacao-components-evitar-acesso-direto"></a>
* **Evitar o Uso de `ViewChild` e `ViewChildren`:**
* Acessar elementos ou instâncias de componentes filhos diretamente com **`ViewChild`** **QUEBRA** o encapsulamento e cria um **acoplamento forte** entre os componentes.
```ts
@ViewChild(FiltroComponent) filtro!: FiltroComponent;
aplicarFiltro() {
this.filtro.atualizarLista(); // chamada direta de método interno
}
```
#### Anti-Performance e Impureza<a name="comunicacao-components-evitar-acesso-impureza"></a>
* **Evitar o Acesso a Métodos de Serviço no Template:**
* Chamar métodos de serviço diretamente no template (ex: `{{ service.getDados() }}`) pode causar **processamento desnecessários** ou chamadas de API repetidas durante o ciclo de detecção de mudanças.
* A lógica DEVE ser movida para o componente (`.ts`), e os dados devem ser expostos como **Observable (AsyncPipe)** ou **Signal**.
---
## Diretrizes de Estilização<a name="estilizacao"></a>
### Tema PrimeNG<a name="estilizacao-tema"></a>
- Adotar uma arquitetura modular de design tokens e temas, centralizando cores, tipografia, espaçamentos e componentes personalizados.
- Preferir a personalização é feita a partir dos design tokens do PrimeNG.
#### Estrutura<a name="estilizacao-tema-estrutura"></a>
```
src/
└── styles/
├── theme.scss # Tema global: gera variáveis CSS e aplica fontes, espaçamentos e cores.
├── components/
│ ├── index.scss # Ponto de entrada para os estilos de componentes. (Expõe o CSS para o thema)
│ ├── buttons/ # Estilos específicos de botões.
│ ├── form/ # Campos de formulário.
│ ├── tooltips/ # Tooltips customizados.
│ └── message/ # Estilos de mensagens de validação ou feedback.
└── variables/
├── _pallete.scss # Tokens de cor.
├── _spacing.scss # Tokens de espaçamento e borda.
├── _typography.scss # Tokens e mixins de tipografia.
```
#### _palette.scss <a name="estilizacao-tema-palette"></a>
- Define o sistema de cores da aplicação.
- Cada cor (como $teal, $red, $gray) é um mapa de 10 tonalidades (50–950).
**Exemplo**:
```scss
$teal: (
50: #f1fdfa,
100: #d1faf1,
200: #a4f5e4,
300: #73e8d3,
400: #50d2be,
500: #3eb6a5,
600: #309387,
700: #27756e,
800: #215d59,
900: #1e4d4a,
950: #0c2f2e,
);
```
- As cores principais do sistema são expostas utilizando variáveis definidas nesse arquivo:
```scss
$primary: $teal;
$success: $green;
$info: $sky;
$warning: $amber;
$danger: $red;
$gray: $slate;
$black: #000;
$white: #fff;
```
- Prefira usar tokens do PrimeNG como `--p-primary-color`, `--p-inputtext-border-color`, etc.
- Sempre extraia valores com map.get(), por exemplo:
**Exemplo**
```scss
@use "sass:map";
@use "../../variables/pallete" as pallete;
--p-button-primary-background: #{map.get(pallete.$primary, 50)};
```
#### _spacing.scss <a name="estilizacao-tema-spacing"></a>
- Centraliza unidades de espaçamento, bordas e sombras.
```scss
$spacing-unit: 8px;
$spacing: (
xxs: $spacing-unit * 0.25,
xs: $spacing-unit * 0.5,
sm: $spacing-unit * 1,
md: $spacing-unit * 1.5,
lg: $spacing-unit * 2,
xl: $spacing-unit * 3,
xxl: $spacing-unit * 4,
xxxl: $spacing-unit * 6,
);
```
**Exemplo:**
```scss
@use "sass:map";
@use "../../variables/spacing" as spacing;
--p-textarea-padding-x: #{map.get(spacing.$spacing, md)};
--p-textarea-padding-y: #{map.get(spacing.$spacing, lg)};
--p-textarea-border-radius: #{spacing.$border-radius};
```
#### _typography.scss <a name="estilizacao-tema-typography"></a>
- Controla a escala tipográfica e hierarquia de texto.
- Inclui
- Mapas `$typography` e `$typography-scale`;
- Mixins: `@mixin apply-font($level, $variant)` e `@mixin generate-typography-vars($map)`
```scss
@mixin generate-typography-vars($map) {
@each $level, $weights in $map {
@each $variant, $props in $weights {
$size: map.get($props, size);
$line-height: map.get($props, line-height);
$weight: map.get($props, weight);
--font-#{$level}-#{$variant}-size: #{$size};
--font-#{$level}-#{$variant}-line-height: #{$line-height};
--font-#{$level}-#{$variant}-weight: #{$weight};
}
}
}
@mixin apply-font($level, $variant) {
font-family: $font-family-base;
font-size: var(--font-#{$level}-#{$variant}-size);
line-height: var(--font-#{$level}-#{$variant}-line-height);
font-weight: var(--font-#{$level}-#{$variant}-weight);
}
```
**Exemplos**:
```scss
@use "../../variables/typography" as typo;
@include typo.apply-font(2, regular);
line-height: #{typo.$field-line-height};
min-height: #{typo.$field-min-height};
```
- Níveis de 1 a 8 correspondem a tamanhos crescentes de texto.
Use regular, medium ou bold para variar o peso.
#### thema.scss <a name="estilizacao-tema-theme"></a>
- É o núcleo de integração com o PrimeNG.
- Importa as variáveis de cor, espaçamento e tipografia.
- - Gera variáveis CSS --p-* que o PrimeNG reconhece como design tokens.
- Define estilos base (`body`,`h1–h6`,`p` ,`small`).
**Exemplo:**
```scss
@use "sass:map";
@use "./variables/pallete" as colors;
@use "./variables/spacing" as spacing;
@use "./variables/typography" as typography;
@use "components/index" as components;
:root {
--p-primary-color: #{map.get(colors.$primary, 500)};
--p-text-color: #{colors.$text-color};
@include typography.generate-typography-vars(typography.$typography-scale);
}
body {
background-color: colors.$surface-ground;
color: colors.$text-color;
@include typography.apply-font(2, regular);
}
```
#### Diretório Components <a name="estilizacao-tema-components"></a>
- Organiza estilos específicos de cada componente customizado (ex: app-textfield, app-dropdown, app-message).
**Exemplo:**
```scss!
app-textfield {
input[pInputText] {
@include typo.apply-font(2, regular);
--p-inputtext-border-color: #{map.get(pallete.$gray, 300)};
--p-inputtext-focus-border-color: #{map.get(pallete.$primary, 500)};
}
}
```
#### index.scss <a name="estilizacao-tema-index"></a>
- Funciona como ponto de agregação para todos os estilos de componentes:
```scss!
@use "./buttons/button.scss";
@use "./form/text-field.scss";
@use "tooltips/tooltip.scss";
@use "message/message.scss";
```
- Esse arquivo é importado diretamente no theme.scss, o que garante que todos os componentes customizados herdem os tokens e estilos globais.
### Tailwind CSS<a name="estilizacao-tailwind"></a>
- Utilizar classes do Tailwind para CSS utilitários e responsividade.
- Criar tokens CSS (design-system) para cores, espaçamentos, tipografia e mapear para Tailwind config (custom properties).
---
## Diretrizes de Comunicação com Back-End<a name="comunicacao-back"></a>
### API Layer (**.api.ts)<a name="comunicacao-back-api-layer"></a>
**Responsabilidade:** Executar chamadas HTTP puras, sem qualquer lógica de negócio.
**✅ O que DEVE ter:**
- Chamadas HTTP diretas (httpClient.get/post/put/delete)
- Configuração de URLs e endpoints
- Configuração de headers HTTP
- Tipagem com DTOs de Request/Response
- Link para documentação Swagger do backend
**❌ O que NÃO deve ter:**
- Tratamento de exceções (delegado ao Facade)
- Transformação de dados (delegado ao Facade)
- Lógica de negócio
- Retry/fallback
- Cache
**Exemplo**:
```ts
@Injectable({
providedIn: 'root',
})
export class ProdutoApi {
private readonly baseUrl = environment.url;
private http = inject(HttpClient);
public getById(id: number): Observable<ProdutoApiResponse> {
return this.http.get<ProdutoApiResponse>(`${this.baseUrl}/${id}`);
}
public create(request: CriarProdutoApiRequest): Observable<void> {
return this.http.post<void>(this.baseUrl, request);
}
}
```
### API Facade Layer (**.api.facade.ts)<a name="comunicacao-back-api-facade-layer"></a>
**Responsabilidade:** Orquestrar chamadas à API, tratar exceções e transformar dados entre DTOs externos e internos.
**✅ O que DEVE ter:**
- Injeção do serviço API correspondente
- Tratamento de exceções HTTP
- Transformação de DTOs de Request (Internal → Request)
- Transformação de DTOs de Response (Response → Internal)
- Lógica de retry/fallback (quando necessário)
- Cache local (quando necessário)
- Mensagens de erro amigáveis
**❌ O que NÃO deve ter:**
- Chamadas HTTP diretas (delegado ao API)
- Lógica de negócio complexa (delegado a Services)
- Acesso direto a componentes
**Exemplo**:
```ts
@Injectable({
providedIn: 'root',
})
export class ProdutoApiFacade {
private produtoApi = inject(ProdutoApi);
private notificacaoService = inject(NotificacaoService);
public getProdutoById(id: number): Observable<ProdutoDto | null> {
return this.produtoApi.getById(id).pipe(
map((response: ProdutoApiResponse) => this.transformResponseToDto(response)),
catchError((error) => {
console.error('Falha ao buscar produto:', error);
this.notificacaoService.exibirErro('Não foi possível carregar o produto. Tente novamente.');
return of(null);
})
);
}
public criarProduto(produto: ProdutoDto): Observable<boolean> {
const request = this.transformDtoToRequest(produto);
return this.produtoApi.create(request).pipe(
tap(() => this.notificacaoService.exibirSucesso('Produto criado com sucesso!')),
map(() => true),
catchError((error) => {
console.error('Falha ao criar produto:', error);
this.notificacaoService.exibirErro('Não foi possível criar o produto.');
return of(false);
})
);
}
private transformResponseToDto(res: ProdutoApiResponse): ProdutoDto {
return new ProdutoDto({
id: res.id_produto,
nome: res.nome_produto,
precoUnitario: res.preco_unitario,
disponivel: res.disponivel_em_estoque,
dataCadastro: new Date(res.data_cadastro),
});
}
private transformDtoToRequest(dto: ProdutoDto): CriarProdutoApiRequest {
return {
nome_produto: dto.nome,
preco_unitario: dto.precoUnitario,
disponivel: dto.disponivel,
};
}
}
```
### DTOs de Request (`**.api.request.ts`)<a name="comunicacao-back-dto-request"></a>
**Responsabilidade:** Espelhar exatamente os DTOs de Request do backend Java.
**✅ Regras:**
- **Espelhar exatamente** a estrutura do backend
- **Nomes devem refletir** os nomes do backend
- **Apenas leitura** - não devem ser modificados em componentes
- **Documentação** com link para o DTO Java correspondente
**Exemplo**:
```ts
/**
* @see endereço_do_swagger
*/
export interface CriarProdutoApiRequest {
nome_produto: string;
preco_unitario: number;
disponivel: boolean;
}
```
### DTOs de Response (`**.api.response.ts`)<a name="comunicacao-back-dto-response"></a>
**Responsabilidade:** Espelhar exatamente os DTOs de Response do backend Java.
**✅ Regras:**
- **Espelhar exatamente** a estrutura do backend
- **Nomes devem refletir** os nomes do backend
- **Apenas leitura** - não devem ser modificados em componentes
- **Documentação** com link para o DTO Java correspondente
**Exemplo**:
```ts
/**
* @see endereço_do_swagger
*/
export interface ProdutoApiResponse {
id_produto: number;
nome_produto: string;
preco_unitario: number;
disponivel_em_estoque: boolean;
data_cadastro: string;
}
```
### DTOs Internos (`**.dto.ts`)<a name="comunicacao-back-dto-interno"></a>
**Responsabilidade:** DTOs usados internamente no frontend, podem ter propriedades calculadas e formatos específicos do Angular.
**✅ Características:**
- Podem ter propriedades **derivadas** ou **calculadas**
- Podem ter formatação específica do frontend
- Podem ter métodos auxiliares
- São usados entre componentes, services e facades
**Exemplo**:
```ts
export class ProdutoDto {
id: number;
nome: string;
precoUnitario: number;
disponivel: boolean;
dataCadastro: Date;
constructor(data: Partial<ProdutoDto>) {
Object.assign(this, data);
}
get statusLabel(): string {
return this.disponivel ? 'Disponível' : 'Indisponível';
}
}
```
---
## Diretrizes para o Tratamento de Exceções<a name="tratamento-excecoes"></a>
### Camadas de tratamento de erros:
| Camada | Mecanismo Principal | Tipo de Erro Capturado | Objetivo Principal
| :--- | :--- | :--- | :--- |
| **Serviços/Componentes** | **RxJS operador catchError** | Erros **específicos** da lógica, validação, falha de API. | Recuperação, Retries, manter o Contexto.
| **Rede (Global)** | **HttpInterceptor** | HttpErrorResponse |Lidar com autenticação/autorização, erros transversais, e logging global |
| **Aplicação (Global)** | **CustomErrorHandler** | Erros de Runtime Não Capturados, Erros de Componentes, Erros do Framework | Logging de telemetria, fallback final, alerta de estado crítico |
- O operador catchError do RxJS deve ser o mecanismo primário para interceptar erros em Observables (como aqueles retornados pelo HttpClient) dentro da camada de Serviços.
**Exemplo:**
```ts
public getData(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
retry(3),
catchError((error: HttpErrorResponse) => {
if (error.status === 503) {
console.warn('Serviço temporariamente indisponível, tente novamente em alguns minutos', error);
return of();
}
return throwError(() => error);
})
);
}
```
- Interceptors para tratamento centralizado, devem transformar erros HTTP em classes padrão de erro.
**Deve**:
* Capturar erros 401 (Não Autorizado) e 403 (Proibido) para implementar logout automático, purga de tokens e redirecionamento para a página de login.
* Enviar logs de erro de rede para ferramentas de telemetria, garantindo que todos os erros HTTP sejam registrados.
* Exibir notificações de erro genéricas para falhas críticas de servidor que não exigem tratamento específico pelo Serviço.
```ts
export const errorHandlerInterceptor: HttpInterceptorFn = (req, next) => {
const TOASTER_SERVICE = inject(ToasterService);
const ROUTER = inject(Router);
return next(req).pipe(
catchError((errorResponse: HttpErrorResponse) => {
if (errorResponse instanceof HttpErrorResponse) {
TOASTER_SERVICE.exibirMensagemErro({
titulo: 'TITULO_ERRO',
mensagem: 'MENSAGEM_ERRO',
});
if (errorResponse.status == 401) {
ROUTER.navigate([Rotas.LOGIN.login], {
queryParams: { returnUrl: ROUTER.routerState.snapshot.url }
});
}
}
return throwError(() => errorResponse);
})
);
};
```
- O ErrorHandler captura erros lançados em contextos onde o framework está no controle, como durante a detecção de mudanças ou a inicialização de componentes..
**Exemplo:**
```ts
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
private _environment = environment;
private _logger: AppLoggerService = inject(AppLoggerService);
private _notificationService: NotificationService = inject(NotificationService);
public handleError(error: any): void {
this._notificationService.notifyCriticalError('Ocorreu um erro inesperado. Tente novamente.');
this._logger.logError(error);
if(!this._environment.isProduction){
console.error('Erro não tratado capturado pelo CustomErrorHandler:', error);
}
}
}
```
**Deve:**
- Exibir mensagens visuais consistentes.
- Evitar a quebra da aplicação por exceções não tratadas.
- Registrar erros relevantes para análise posterior.
___
## Diretrizes para implementação de testes<a name="testes"></a>
### Configuração de Testes com o Array imports<a name="testes-imports"></a>
* A principal alteração na configuração de testes para componentes standalone reside no TestBed.configureTestingModule. Em vez de importar um NgModule que declara o componente, o próprio componente standalone é adicionado diretamente ao array imports.
```ts
await TestBed.configureTestingModule({
imports:,
providers: [
//... mocks de serviços
]
}).compileComponents();
```
### Padrões para Testar componentes: <a name="testes-padroes"></a>
* Diretivas de Atributo:
* A abordagem padrão para testar diretivas é criar um componente de teste mínimo(_HostComponent_)
```ts
@Component({
template: `<div appHighlight>Texto a ser destacado</div>`
})
class TestHostComponent {}
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestHostComponent>;
let divElement: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations:
});
fixture = TestBed.createComponent(TestHostComponent);
divElement = fixture.debugElement.query(By.css('div'));
});
});
```
### Boas Práticas <a name="testes-boas-praticas"></a>
**Deve**
* Utilizar o padrão AAA: Arrange(Organizar), Act (Agir) e Assert (Afirmar) para montar os cenários.
* Os testes devem validar a funcionalidade esquecendo detalhes de implementação.
**Exemplo:**
`it('deve chamar o método privado _recalcularTotal ao adicionar um item',...)` Frágil.
`it('deve exibir o novo valor total no DOM quando um item é adicionado',...)`
* Ao testar componentes 'Conteiner' usar overrideComponente para remover a importação do componente filho e usar um mock em seu lugar.
```ts
@Component({
selector: 'app-filho-real', //-> Mesmo seletor do componente real
standalone: true,
template: '<div>Componente Filho Mockado</div>'
})
class FilhoMockComponent {}
// No cenário de teste
await TestBed.configureTestingModule({
imports: [PaiComponent],
})
.overrideComponent(PaiComponent, {
remove: { imports: },
add: { imports: [FilhoMockComponent] }
})
.compileComponents();
```
**Não deve:**
* Depender de estados deixados por testes anteriores.
* Agrupar vários cenários em um único teste.
* Precisar de uma quantidade excessiva de mocks. Esse ponto é um alerta que a classe ou componente possui muitas responsabilidades.
* Ser instáveis, deve manter o resultado de forma consistentente.
#### Organização e Nomeclatura <a name="testes-organizacao-nomeclatura"></a>
* Por convenção do Angular CLI, os arquivos de teste devem ser nomeados com o sufixo .spec.ts e colocados ao lado do arquivo que estão testando `usuario-listagem.component.ts` -> `usuario-listagem.component.spec.ts`
* As descrições nos blocos describe e it devem ser claras e formar uma frase legível que documente o comportamento esperado.
```ts
describe('UnidadeSobTeste', () => { it('deve [fazer algo] quando [em uma certa condição]', () => {... }); });.
```
___
## Melhores Práticas de Implementação<a name="melhores-praticas-de-implementacao"></a>
**Deve**:
- Usar apenas os atributos html/template e css necessários para evitar acumulo de "lixo" no código.
- Resolver todos os code smells apontados pelo Sonarqube.
- Tentar resolver todos os warnings de qualidade de código relevantes apontados pela IDE.
- Manter tipagem concisa e reutilizavel.
- Usar `trackBy` em @For para performance.
- Configurar tsconfig.json da forma que melhor enforce as boas praticas de programação com Typescript.
- Configurar angular.json da forma que melhor enforce as boas praticas de programação com Typescript.
- Usar `const` em vez de `let` quando o valor não será reatribuído, reforçando intenção de imutabilidade.
**Não Deve**:
- Não Acessar DOM Diretamente. Usar Renderer2 ou HostBinding
- Não Realizar Lógica JavaScript no Template. Mover lógica para o componente ou classe responsável.
- Inserir HTML ou URLs diretamente no templete sem sanitização.
- Realizar operações custosas dentro do ciclo de vida do Angular (ex.: loops complexos em _ngOnInit_ ou _ngAfterViewInit_).
- Carregar módulos sem _lazy loading_.
- Encadear um subscribe dentro de outro. (Use operadores de encadeamento do RxJs).
- Usar subscribes encadeados dentro de outros.
- Usar`any` sem necessidade.
## Glossário<a name="glossario"></a>
| Tópico/Seção | Convenção de Nomenclatura | Exemplo(s) |
| :--- | :--- | :--- |
| **Reatividade (RxJS)** | Variáveis do tipo `Observable` devem terminar com o sufixo `$` (dólar). | `dividas$`, `isLoading$`, `usuarioLogado$` |
| **Reatividade (ReactiveForms)** | Interfaces ou Types que definem a estrutura de um `FormGroup` devem ser prefixados com `IForm` ou `Form`. | `IFormUsuario`, `FormCliente` |
| **Componentização (Container)** | Arquivos de *Smart Components* (Containers) devem usar o sufixo `.container.ts`. | `cadastro-inscricao-divida.container.ts` |
| **Componentização (Presenter)** | Arquivos de *Dumb Components* específicos de uma feature (Presenters) devem usar o sufixo `.presenter.ts`. | `cadastro-inscricao-divida-list.presenter.ts` |
| **Comunicação com Back-End (API Layer)** | Arquivos que realizam chamadas HTTP puras devem usar o sufixo `.api.ts`. A classe segue o padrão `[NomeDoRecurso]Api`. | `ProdutoApi` em `produto.api.ts` |
| **Comunicação com Back-End (API Facade)** | Arquivos que orquestram a camada de API, tratam erros e transformam dados devem usar o sufixo `.api.facade.ts`. A classe segue o padrão `[NomeDoRecurso]ApiFacade`. | `ProdutoApiFacade` em `produto.api.facade.ts` |
| **Comunicação com Back-End (DTOs de Request)** | Arquivos de DTOs de *Request* que espelham o backend devem usar o sufixo `.api.request.ts`. A interface segue o padrão `[Acao][Recurso]ApiRequest`. | `CriarProdutoApiRequest` em `criar-produto.api.request.ts` |
| **Comunicação com Back-End (DTOs de Response)** | Arquivos de DTOs de *Response* que espelham o backend devem usar o sufixo `.api.response.ts`. A interface segue o padrão `[Recurso]ApiResponse`. | `ProdutoApiResponse` em `produto.api.response.ts` |
| **Comunicação com Back-End (DTOs Internos)** | Arquivos de DTOs para uso interno no frontend devem usar o sufixo `.dto.ts`. A classe segue o padrão `[Recurso]Dto`. | `ProdutoDto` em `produto.dto.ts` |
| **Testes (Arquivos)** | Arquivos de teste devem usar o sufixo `.spec.ts` e estar localizados ao lado do arquivo que estão testando. | `usuario-listagem.container.spec.ts` |
| **Testes (Descrições)** | As descrições nos blocos `describe` e `it` devem ser claras e formar uma frase legível que documente o comportamento esperado. | `describe('UnidadeSobTeste', () => { it('deve [fazer algo] quando [em uma certa condição]', ...); });` |