Pular para conteúdo

Guia de Boas Práticas: Desenvolvimento Front-End (Angular)


1. Filosofia de Desenvolvimento

  • Código para Leitura: O código deve ser escrito de forma que sua intenção seja compreensível rapidamente apenas pela leitura. Não deve depender de explicações verbais, comentários excessivos ou do conhecimento prévio de quem o escreveu. A legibilidade é priorizada em relação à brevidade, aceitando-se o uso de mais linhas de código e nomes mais descritivos sempre que isso contribuir para tornar a intenção e o funcionamento da solução mais evidentes.
  • Simplicidade Necessária: Evite abstrações precoces. A complexidade só deve ser introduzida quando o problema atual exigir, e não com base em possíveis necessidades futuras.
  • Consistência de Grupo: O código deve seguir padrões consistentes de escrita, organização e nomenclatura, independentemente de quem o desenvolveu. O objetivo é que a base de código mantenha um estilo uniforme, facilitando leitura, revisão e manutenção.

2. Padrões de Escrita e TypeScript

Nomenclatura Intencional

  • Motivação\ Nomes claros e expressivos reduzem ambiguidades, facilitam a leitura do código e diminuem o custo de manutenção. Uma boa nomenclatura comunica intenção, contexto e comportamento sem exigir comentários adicionais.
  • Resolução
  • Utilizar nomes que expressem claramente intenção e estado.
  • Variáveis booleanas devem indicar condição ou propósito (isSubmitButtonDisabled em vez de disabled).
  • Evitar abreviações obscuras (user em vez de u).
  • Exemplos
// Ruim
const d = true;

// Bom
const isSubmitButtonDisabled = true;

O Fim do Tipo Oculto (any)

  • Motivação\ O uso de any elimina a verificação de tipos em tempo de compilação, permitindo que erros apareçam apenas em tempo de execução. Isso aumenta riscos, dificulta refatorações e reduz a confiabilidade do código.
  • Resolução
  • O uso de any é proibido.
  • Sempre definir interface ou type adequados para representar dados.
  • Exemplos
// Uso incorreto com any
function getUserName(user: any): string {
  return user.name; // typo não é detectado pelo compilador
}
// Uso correto com interface
interface User {
  id: number;
  name: string;
  email: string;
}

function getUserName(user: User): string {
  return user.name; // erro em compilação se 'name' não existir
}

Tipagem Estrita de Retorno

  • Motivação\ Declarar explicitamente o tipo de retorno evita efeitos colaterais em cadeia, facilita o entendimento da API e permite que o compilador identifique inconsistências mais cedo.
  • Resolução
  • Todas as funções e métodos devem declarar explicitamente o tipo de retorno.
  • Evitar inferência implícita em métodos públicos e serviços.
  • Exemplos
// Exemplo incorreto
getUser() {
  return this.http.get('/api/users/1');
}
// Exemplo correto
interface User {
  id: number;
  name: string;
}

getUser(): Observable<User> {
  return this.http.get<User>('/api/users/1');
}

Tratamento de Nulos (null e undefined)

  • Motivação\ Acessos a valores nulos ou indefinidos são uma das principais causas de erros em tempo de execução. O tratamento explícito torna o código mais seguro e previsível.
  • Resolução
  • Tratar explicitamente cenários de null e undefined.
  • Utilizar Optional Chaining (?.) quando o valor pode não existir.
  • Utilizar Nullish Coalescing (??) para definir valores padrão apenas quando o valor for null ou undefined.
  • Exemplos
// Optional Chaining
const city = user?.address?.city;
// Nullish Coalescing
const itemsPerPage = config.itemsPerPage ?? 10;

3. Componentização e Design de UI

O Componente "Puro"

  • Motivação\ Manter componentes simples, previsíveis e fáceis de reutilizar melhora a manutenibilidade, testabilidade e clareza da aplicação. Separar responsabilidades evita acoplamento excessivo entre UI, regras de negócio e infraestrutura.
  • Resolução
  • Componentes devem ser preferencialmente puros (de apresentação).
  • Um componente puro apenas exibe dados e emite eventos.
  • Não deve conter lógica de negócio, chamadas HTTP ou acesso direto a serviços.
  • Dados entram via @Input() e interações saem via @Output().
  • Exemplos
@Component({
  selector: 'app-user-form',
  template: `
    <form (ngSubmit)="onSubmit()">
      <input [value]="user?.name" (input)="onNameChange($event.target.value)" />
      <button [disabled]="isSubmitDisabled">Salvar</button>
    </form>
  `,
})
export class UserFormComponent {
  @Input() user!: User;
  @Input() isSubmitDisabled = false;
  @Output() submitted = new EventEmitter<User>();
  @Output() nameChanged = new EventEmitter<string>();

  onSubmit(): void {
    this.submitted.emit(this.user);
  }

  onNameChange(name: string): void {
    this.nameChanged.emit(name);
  }
}

Observação\ Esse componente não conhece HTTP, não chama services diretamente e não contém regras de negócio (impostos, parsing complexo, etc.).

Gestão de Lógica de Negócio

  • Motivação\ Misturar lógica de negócio com componentes dificulta testes, reutilização e evolução do sistema. Centralizar regras em services promove coesão e reduz duplicações.
  • Resolução
  • Componentes não implementam regras de negócio complexas.
  • Componentes não acessam diretamente infraestrutura (HTTP, storage, etc.).
  • Toda lógica de negócio deve residir em Services.
  • O componente atua apenas como orquestrador.
  • Exemplos
@Component({
  selector: 'app-order-page',
  template: `
    <app-order-form
      [order]="order"
      [total]="total"
      (submitted)="onSubmit($event)">
    </app-order-form>
  `,
})
export class OrderPageComponent implements OnInit {
  order!: Order;
  total = 0;

  constructor(private orderService: OrderService) {}

  ngOnInit(): void {
    this.orderService.getDraftOrder().subscribe(order => {
      this.order = order;
      this.total = this.orderService.calculateTotal(order);
    });
  }

  onSubmit(order: Order): void {
    this.orderService.submit(order).subscribe();
  }
}
@Injectable({ providedIn: 'root' })
export class OrderService {
  getDraftOrder(): Observable<Order> {
    return this.http.get<Order>('/api/orders/draft');
  }

  calculateTotal(order: Order): number {
    return order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  submit(order: Order): Observable<void> {
    return this.http.post<void>('/api/orders', order);
  }
}

Ciclo de Vida com Propósito

  • Motivação\ O uso indiscriminado de hooks do ciclo de vida aumenta complexidade e risco de efeitos colaterais. Cada hook deve ter responsabilidade clara.
  • Resolução
  • Usar apenas os hooks necessários.
  • Cada hook deve ter um propósito bem definido.
  • Exemplos
// constructor: apenas para injeção de dependências simples e inicialização mínima
constructor(private orderService: OrderService) {}
// ngOnInit: carregamento inicial de dados
ngOnInit(): void {
  this.loadOrders();
}
// ngOnChanges: Reagir a mudanças em @Input() (ex.: recalcular valores derivados).
ngOnChanges(changes: SimpleChanges): void {
  if (changes['user']) {
    this.displayName = this.buildDisplayName(this.user);
  }
}
// ngAfterViewInit: interação com ViewChild ou libs de UI
ngAfterViewInit(): void {
  this.table.initialize();
}
// ngOnDestroy: limpeza de recursos
private destroy$ = new Subject<void>();

ngOnInit(): void {
  this.orderService.getOrders()
    .pipe(takeUntil(this.destroy$))
    .subscribe();
}

ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}

Performance com OnPush + Signals

  • Motivação\ Reduzir renderizações desnecessárias melhora performance, especialmente em telas complexas com listas grandes ou componentes aninhados.
  • Resolução
  • Utilizar ChangeDetectionStrategy.OnPush sempre que possível.
  • Preferir signals para gerenciamento de estado local.
  • Atualizar estado de forma imutável.
  • Manter templates simples, com mínima lógica.
  • Exemplos
@Component({
  selector: 'app-counter',
  template: `
    <div>{{ count() }}</div>
    <button (click)="increment()">Incrementar</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
  count = signal(0);

  increment(): void {
    this.count.set(this.count() + 1);
  }
}

Boas práticas adicionais

  • Usar @for com track em listas.
  • Evitar mutações profundas de objetos.
  • Preferir composição a lógica no template.

4. Reatividade e Fluxo de Dados - RxJS

  • Motivação\ Em aplicações reativas, o controle correto do fluxo de dados é essencial para garantir previsibilidade, performance e manutenibilidade. O uso inadequado de variáveis imperativas e subscrições manuais tende a gerar efeitos colaterais, estados inconsistentes e memory leaks.

Programação Declarativa com Observables

  • Motivação\ Variáveis imperativas que mudam de estado ao longo do tempo dificultam o rastreamento do fluxo de dados e aumentam a chance de bugs. Observables permitem modelar dados como streams previsíveis e composáveis.
  • Resolução
  • Preferir Observable em vez de variáveis mutáveis.
  • Evitar subscribe apenas para atribuição de estado.
  • Utilizar o async pipe sempre que possível.
  • Exemplos
// Imperativo (evitar)
let users: User[] = [];

this.userService.getUsers().subscribe(data => {
  users = data;
});
// Declarativo (preferir)
users$ = this.userService.getUsers();
<ul>
  <li *ngFor="let user of users$ | async">
    {{ user.name }}
  </li>
</ul>

Gestão de Subscrições e Prevenção de Memory Leaks

  • Motivação\ Subscrições não finalizadas mantêm referências vivas na memória, causando vazamentos e comportamentos inesperados ao longo do tempo.
  • Resolução
  • Evitar subscribe manual sempre que possível.
  • Utilizar operadores de destruição automática (takeUntil, takeUntilDestroyed).
  • Preferir async pipe, que gerencia o ciclo de vida automaticamente.
  • Exemplos
// Padrão com takeUntil
private destroy$ = new Subject<void>();

ngOnInit(): void {
  this.userService.getUsers()
    .pipe(takeUntil(this.destroy$))
    .subscribe();
}

ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}
// Angular moderno (preferir)
this.userService.getUsers()
  .pipe(takeUntilDestroyed())
  .subscribe();

Tratamento de Erros em Streams

  • Motivação\ Quando um erro não é tratado, o stream é finalizado, podendo "matar" toda a tela ou impedir novas atualizações de dados.
  • Resolução
  • Nunca deixar erros propagarem sem tratamento.
  • Utilizar catchError para capturar falhas.
  • Retornar valores de fallback ou streams alternativos.
  • Exemplos
users$ = this.userService.getUsers().pipe(
  catchError(error => {
    this.notificationService.error('Erro ao carregar usuários');
    return of([] as User[]); // fallback seguro
  })
);
`

Operadores Essenciais (switchMap, mergeMap, exhaustMap)

  • Motivação\ Operadores de flattening controlam como múltiplos Observables internos são executados. A escolha incorreta pode causar chamadas duplicadas, respostas fora de ordem ou ações concorrentes indesejadas.
  • Resolução
  • Usar switchMap quando apenas o resultado mais recente importa.
  • Usar mergeMap quando múltiplas execuções simultâneas são aceitáveis.
  • Usar exhaustMap quando uma nova ação deve ser ignorada enquanto a anterior ainda está em execução.
  • Guia Prático
  • switchMap\ Cancela a execução anterior ao receber um novo valor.\ Ideal para buscas, filtros e autocompletes.
search$ = this.searchTerm$.pipe(
  switchMap(term => this.userService.search(term))
);
  • mergeMap\ Executa todas as requisições em paralelo.\ Ideal para processamento em lote ou ações independentes.
saveAll$ = from(users).pipe(
  mergeMap(user => this.userService.save(user))
);
  • exhaustMap\ Ignora novas emissões enquanto a atual não termina.\ Ideal para submits de formulário e ações de clique.
submit$ = this.submitClick$.pipe(
  exhaustMap(() => this.orderService.submit())
);
  • Resumo de Uso
  • switchMap: evita respostas fora de ordem.
  • mergeMap: permite concorrência controlada.
  • exhaustMap: previne múltiplos disparos simultâneos.

5. Integração com APIs e Resiliência

Padronização com ServiceAbstractClass e Injeção Moderna

  • Motivação A ausência de uma base comum para chamadas HTTP resulta em duplicação de configurações (como a baseUrl), inconsistência no tratamento de headers e dificuldade para implementar mudanças globais na comunicação com o backend. Além disso, a injeção via construtor em classes com herança torna o código verboso e difícil de manter.
  • Resolução
  • Todos os serviços de integração devem estender a ServiceAbstractClass (localizada em src/app/api/), que centraliza o acesso ao HttpClient e à URL base definida no environment.
  • A organização deve seguir o domínio funcional dentro da pasta app/api/ (ex: api/auth, api/users).
  • Utilizar obrigatoriamente a função inject() para injeção de dependências, mantendo o padrão moderno do Angular e simplificando a herança de classes.
  • Exemplo
// src/app/api/users/user.api.service.ts
@Injectable({ providedIn: 'root' })
export class UserApiService extends ServiceAbstractClass {
  private readonly endpoint = 'users';

  // Uso do baseUrl herdado e injeção moderna com inject()
  findAll(): Observable<UserResponse[]> {
    return this.http.get<UserResponse[]>(`${this.baseUrl}/${this.endpoint}`);
  }
}

Tipagem Forte e Isolamento de Contrato

  • Motivação Depender de tipos genéricos (any) ou declarar interfaces dentro de componentes cria um acoplamento perigoso. Se o backend alterar a estrutura de um JSON, a aplicação pode quebrar silenciosamente em múltiplos pontos. O isolamento de tipos garante que o contrato com a API seja uma "fonte única da verdade".
  • Resolução
  • Definir interfaces de entrada e saída em arquivos *.types.ts exclusivos dentro da pasta de cada domínio na camada de API.
  • Nunca declarar tipos ou interfaces dentro de componentes ou serviços de UI; importar sempre das definições da camada de API.
  • Seguir a convenção de nomenclatura de arquivos: nome-do-servico.types.ts.
  • Exemplo
// src/app/api/users/user.types.ts
export interface UserResponse {
  uuid: string;
  display_name: string;
  user_email: string;
}

// No componente de listagem (Consumo seguro e tipado)
users: UserResponse[] = [];

Resiliência e Mapeamento de Erros do Servidor

  • Motivação Erros de API (como falhas de validação 400 ou erros 500) precisam ser comunicados de forma clara. A aplicação deve ser resiliente para não interromper o fluxo do usuário e capaz de mapear mensagens de erro específicas do servidor diretamente para os campos do formulário afetados, evitando feedbacks genéricos e manuais.
  • Resolução
  • Utilizar o FormValidatorService para realizar o mapeamento de erros vindos do servidor para o ReactiveForm através do método setServerErrors.
  • Integrar o fluxo de feedback visual com o ngx-toastr para exibir notificações globais de forma consistente.
  • Utilizar o ngx-translate para garantir que as mensagens de erro tratadas no front-end respeitem a internacionalização.
  • Exemplo
// Exemplo em uma Página de Cadastro (Feature)
this.userApi.create(data).subscribe({
  next: () => {
    this.toastr.success(this.translate.instant('SUCCESS.SAVE'));
    this.router.navigate(['/usuarios']);
  },
  error: (err) => {
    // Mapeia erros 400/422 do servidor para os campos do formulário via helper
    this.validator.setServerErrors(this.form, err);
    this.toastr.error(this.translate.instant('ERROR.VALIDATION_FAILED'));
  }
});

6. Qualidade de Entrega e Auto-Revisão

Validação Funcional: Do Mock à Realidade

  • Motivação Desenvolver utilizando dados "mockados" ou flags manuais (ex: isAdmin = true) é útil para velocidade, mas esquecer de removê-los é fatal. Entregar uma tarefa que funciona apenas no seu ambiente local porque os dados estão hardcoded gera bugs de integração imediatos e perda de confiança na equipe. "Funciona na minha máquina" não é critério de aceite.
  • Resolução
  • Teste Real Obrigatório: Antes de abrir o MR, a funcionalidade deve ser testada conectada à API real de desenvolvimento/homologação.
  • Limpeza de Mocks: Verificar explicitamente se não restaram variáveis forçadas, comentários de debug ou json estático no código.
  • Cenário de Erro: Não testar apenas o "caminho feliz". Verificar se a aplicação quebra graciosamente quando a API retorna erro.
  • Exemplo
// ❌ RUIM: Código enviado com flags de teste esquecidas
// const userRole = 'admin'; // TODO: remover isso depois
const userRole = this.authService.getRole();

// ✅ BOM: O desenvolvedor removeu os mocks e testou o fluxo real
// Se a API estiver fora, o catchError trata a exceção
this.userApi.getProfile().subscribe({
  next: (data) => this.profile.set(data),
  error: (err) => this.toastr.error('Erro ao carregar perfil')
});

Atomidade do MR: Não Misturar Assuntos

  • Motivação Um Merge Request deve resolver um problema. Misturar a implementação de uma nova feature com a correção de um bug antigo em outro arquivo, ou com uma formatação geral de código (limpeza), torna o Review impossível. Se a feature precisar ser revertida, as correções úteis vão junto para o lixo.
  • Resolução
  • Uma Tarefa = Um MR: Se você encontrou um erro em outro módulo enquanto trabalhava, anote, crie um ticket separado ou faça um MR exclusivo para aquilo.
  • Não faça "Faxina" junto com "Obra": Se quiser reidentar um arquivo ou mudar nomes de variáveis para melhorar a leitura, faça isso em um commit/MR separado da lógica de negócio.
  • Contexto Isolado: O revisor deve conseguir entender sua mudança olhando apenas para os arquivos alterados, sem ter que filtrar mentalmente o que é feature e o que é formatação.
  • Exemplo
❌ CENÁRIO RUIM (Tudo misturado no mesmo MR):
Arquivo A: Implementa a lógica do novo botão de Login (Feature)
Arquivo B: Corrige um erro de digitação no Rodapé (Fix não relacionado)
Arquivo C: Reorganiza a indentação de 300 linhas de CSS (Style/Refactor)

✅ CENÁRIO BOM (MRs Separados):
MR #1: fix(footer): corrige erro de digitação
MR #2: feat(auth): implementa botão de login

A Auto-Revisão (O Primeiro Review é Seu)

  • Motivação Enviar código com console.log('teste'), código comentado (// antigo_codigo) ou importações não utilizadas demonstra desleixo. O Code Review da equipe serve para discutir arquitetura e lógica, não para ser fiscal de sujeira. Enviar "lixo" consome o tempo do time e polui o histórico.
  • Resolução
  • Revise seu próprio Diff: Antes de criar o MR, abra a aba "Files Changed" e leia linha por linha do que você está enviando.
  • Remova a Sujeira: Apague logs, debugger, imports não usados e código comentado.
  • Garanta a Integridade: Se você alterou um componente reutilizável, verifique se não quebrou outras telas que o utilizam.
  • Exemplo
// ❌ RUIM: Código enviado "sujo"
export class ProductComponent {
  // api = inject(ApiService); // Por que isso está comentado aqui?

  ngOnInit() {
    console.log('chegou aqui'); // Lixo de debug
    debugger; // Parada de execução esquecida
  }
}

// ✅ BOM: Código limpo e pronto para revisão
export class ProductComponent {
  private api = inject(ApiService);

  ngOnInit() {
    this.loadProducts();
  }
}

7. Estilização e Design System

  • Objetivo: garantir que a interface de usuário seja escalável, consistente, performática e adaptável a diferentes temas. Assegurar consistência visual, manutenção eficiente, fidelidade ao design e melhor performance

  • Arquitetura de CSS

  • Motivação: Estilos precisam ser modulares e isolados para garantir escalabilidade e manutenção a longo prazo. A manipulação direta de estilos via JavaScript deve ser evitada para prevenir bugs e comprometer a performance.

  • Resolução:
  • Utilizar SCSS ou CSS Modules para garantir escopo isolado de estilos.
  • Métodos como BEM ou OOCSS são recomendados para garantir clareza e reutilização.
  • Estilos devem ser definidos no componente (com .component.scss) e não em arquivos globais.
@import 'src/app/@ssp/dipol/shared/lib/styles/partials/variables';

.alertDelete {
  background: $red-00;
  border-left: 10px solid $red-20;
  padding: 5px 15px;
  margin-bottom: 20px;
  .message {
    display: flex;
    align-items: center;
    margin: 0;
    i {
      width: 36px;
      height: 36px;
      background: $white;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 50px;
      color: $red-20;
      font-size: 20px;
      margin-right: 15px;
    }
  }
}
  1. Design Tokens

  2. Motivação: Para garantir consistência visual e fidelidade ao design, é essencial usar variáveis reutilizáveis que definem cores, espaçamentos, tipografia e outros aspectos visuais.

  3. Resolução:
  4. Utilizar design tokens para padronizar atributos como cores, tamanhos de espaçamento, tipografia, bordas, etc.
  5. Os tokens podem ser definidos como variáveis SCSS ou objetos JSON no caso de design systems mais complexos.

// Definindo design tokens em SCSS
$primary-color: #007bff;
$secondary-color: #6c757d;
$spacing-unit: 16px;
$font-family: 'Roboto', sans-serif;

// Usando design tokens no componente
.my-component {
  font-family: $font-family;
  padding: $spacing-unit;
  color: $primary-color;
}
3. Componentes customizáveis

  • Motivação: Componentes customizáveis e reutilizáveis são essenciais para criar interfaces modulares e escaláveis. O uso de bibliotecas de UI deve ser balanceado com a criação de componentes internos quando necessário.
  • Resolução:
  • Utilizar bibliotecas de UI como Angular Material ou TailwindCSS para acelerar o desenvolvimento de componentes, mas com flexibilidade para customizar quando necessário.
  • Evitar a criação de componentes completamente do zero, a menos que seja necessário para flexibilidade ou requisitos específicos do produto.
// Exemplo de uso do Angular Material com customização de tema
import { MatButtonModule } from '@angular/material/button';

@NgModule({
  imports: [MatButtonModule],
})
export class AppModule {}
// Customizando um botão do Angular Material
.mat-button {
  background-color: $primary-color;
  color: white;
}
  1. Dark Mode e Tematização

  2. Motivação: A suporte a múltiplos temas (como Dark Mode) é uma exigência comum em aplicações modernas. A implementação deve ser flexível, sem duplicação de lógica e com boas práticas de gerenciamento de temas.

  3. Resolução:
  4. Criar uma estrutura de temas dinâmicos usando CSS custom properties (variáveis) para suportar facilmente múltiplos temas.
  5. Evitar duplicação de lógica, centralizando a configuração de temas em um único arquivo e alterando somente variáveis de design (cores, fontes, etc.).
// Definindo variáveis para temas (Light e Dark)
:root {
  --primary-color-light: #007bff;
  --primary-color-dark: #1e1e1e;
}

[data-theme='light'] {
  --primary-color: var(--primary-color-light);
}

[data-theme='dark'] {
  --primary-color: var(--primary-color-dark);
}

// Usando variáveis de tema no componente
.my-component {
  background-color: var(--primary-color);
}
// Trocando o tema em tempo de execução
toggleTheme() {
  const currentTheme = document.documentElement.getAttribute('data-theme');
  document.documentElement.setAttribute('data-theme', currentTheme === 'light' ? 'dark' : 'light');
}

8. Gestão de Ativos e Performance

  • Objetivo: Garantir que os recursos sejam carregados eficientemente sem comprometer a experiência do usuário. Assegurar o carregamento eficiente de recursos, divisão inteligente de código e otimização contínua da performance.
  • Lazy Loading

    * Motivação: A divisão do código em módulos carregados sob demanda pode reduzir significativamente o tamanho do bundle inicial, melhorando o tempo de carregamento da aplicação.

    * Resolução: * Dividir a aplicação em módulos preguiçosos (lazy-loaded) utilizando o Angular Router. * Módulos que não são críticos para o carregamento inicial devem ser carregados sob demanda.

const routes: Routes = [
  {
    path: 'perfil',
    loadChildren: () => import('./pages/meu-perfil/meu-perfil.module').then(m => m.MeuPerfilModule),
  },
];
  1. Estratégia de Assets

  2. Motivação: O uso inadequado de formatos de imagem e organização de arquivos pode prejudicar o desempenho da aplicação, afetando o tempo de carregamento e a experiência do usuário.

  3. Resolução:
  4. Utilizar formatos de imagem otimizados como WebP para imagens e SVG para ícones.
  5. Organizar os assets em pastas estruturadas e separar por tipo de recurso (ex.: assets/images/, assets/icons/).
  6. Minificar e compactar imagens e fontes para reduzir o tamanho do download.
// Estrutura de pastas de assets
assets/
  images/
    logo.webp
    banner.svg
  fonts/
    roboto.woff2
  icons/
    close-icon.svg
// Exemplo de otimização com WebP em templates
<img src="assets/images/logo.webp" alt="Logo">
  1. Bundle Size

* Motivação: Manter o tamanho do bundle sob controle é essencial para garantir um tempo de carregamento rápido, especialmente em dispositivos móveis.

* Resolução: * Monitorar o tamanho do bundle regularmente. * Evitar o uso de bibliotecas grandes sem necessidade (ex: não importar tudo de uma biblioteca). * Usar ferramentas como webpack-bundle-analyzer para identificar e otimizar o bundle.

// Evitar importação de toda a biblioteca
import { MatButtonModule, MatIconModule, MatInputModule } from '@angular/material';

// Importar apenas o necessário
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';

9. Segurança no Frontend

  • Objetivo: Garantir que a aplicação seja resiliente a ataques comuns, proteja dados sensíveis e minimize riscos de vulnerabilidades, sem sacrificar a experiência do usuário.
  • Autenticação e Autorização

    * Motivação: Rotas e funcionalidades sensíveis não devem ser acessíveis por usuários não autenticados ou sem permissão adequada.

    * Resolução: * Use guards para proteger rotas (AuthGuard, RoleGuard). * Validar permissões no frontend para melhorar UX. * Tokens devem ser armazenados de forma segura.

canActivate(): Observable<boolean> {
  return this.authService.isLoggedIn().pipe(
    first(),
    map((authenticated: boolean) => {
      if (!authenticated) {
        this.router.navigate(['/']);
        return false;
      } else {
        return true;
      }
    }),
  );
}
  1. Validação de Dados

* Motivação: Entradas inválidas podem causar falhas, inconsistências e facilitar ataques.

* Resolução: * Validar todos os dados do usuário antes de enviar ao backend. * Usar form validators e schemas de validação (ex.: Yup ou Angular Validators). * Nunca substituir validações obrigatórias do backend.

import * as yup from 'yup';

export const userSchema = yup.object({
  name: yup
    .string()
    .required('Nome é obrigatório')
    .min(3, 'Nome deve ter pelo menos 3 caracteres'),
});

// FORM ANGULAR

form = this.fb.group({
  name: ['', [Validators.required, Validators.minLength(3)]],
});
  1. Proteção contra XSS (Cross-Site Scripting)

* Motivação: Execução de scripts maliciosos pode comprometer sessões, dados e credenciais.

* Resolução: * Nunca insira HTML arbitrário diretamente com [innerHTML]. * Sanitize dados provenientes de APIs externas. * Utilizar DomSanitizer apenas quando necessário.

const html = `Processando assinatura <i class="pi pi-spin pi-spinner"></i>`;
return this.sanitizer.bypassSecurityTrustHtml(html);
4. Proteção contra CSRF (Cross-Site Request Forgery)

* Motivação: Requisições maliciosas podem ser executadas sem o consentimento do usuário autenticado.

* Resolução: * Utilizar cookies de sessão. * Permitir que o backend valide tokens CSRF. * Utilizar HttpClient com envio automático de cookies.

this.http.post('/api/acao', dados, { withCredentials: true });
  1. Proteção contra CSRF (Cross-Site Request Forgery)

* Motivação: Execução de código dinâmico abre portas para ataques críticos.

* Resolução: * Proibir uso de eval(), new Function() e scripts dinâmicos. * Utilizar apenas bindings e estruturas declarativas do Angular.

<button (click)="executar()">Executar</button>
executar() {
  this.service.acao();
}

  1. Segurança de Armazenamento Local

* Motivação: Dados armazenados no navegador podem ser acessados em ataques XSS.

* Resolução: * Não salvar dados sensíveis em localStorage ou sessionStorage sem criptografia. * Preferir cookies seguros e HttpOnly.

// EVITAR
login() {
  localStorage.setItem('access_token', token);
}
  1. Tratamento de Erros e Logs

* Motivação: Mensagens detalhadas podem expor informações internas da aplicação.

* Resolução: * Nunca expor stack trace ou dados sensíveis na tela. * Logs de erros devem ser anônimos e enviados de forma segura para monitoramento. * Exibir mensagens genéricas ao usuário em caso de erro não esperado.

catchError(() => {
  this.toast.error('Erro inesperado. Tente novamente.');
  return;
});

10. Acessibilidade como Padrão

Semântica HTML e Navegação por Teclado

  • Motivação A acessibilidade não é apenas uma "feature extra", é um requisito básico de qualidade. O uso incorreto de tags (ex: div com clique em vez de button) impede que leitores de tela interpretem a página e bloqueia a navegação de usuários que dependem exclusivamente do teclado.
  • Resolução
  • HTML Semântico: Utilizar as tags corretas para a estrutura (main, nav, footer, article).
  • Interatividade Nativa: Para ações de clique, utilizar sempre <button> ou <a>. Se for inevitável usar uma div clicável, ela deve ter tabindex="0" e tratamento de eventos de teclado (keydown.enter).
  • Foco Visível: Nunca remover o outline de foco via CSS sem fornecer uma alternativa visual clara (box-shadow ou borda).
  • Exemplo
<div (click)="salvar()">Salvar</div>

<button type="button" class="btn btn-primary" (click)="salvar()">
  Salvar
</button>

<div 
  role="button" 
  tabindex="0" 
  (click)="toggle()" 
  (keydown.enter)="toggle()"
  aria-label="Alternar painel">
  Expandir
</div>

Atributos ARIA e Rótulos

  • Motivação Ícones isolados (como um "X" para fechar ou uma lupa para buscar) são invisíveis para quem usa leitores de tela se não tiverem rótulos. Campos de formulário sem label associado também geram barreiras de uso.
  • Resolução
  • Aria-label: Utilizar aria-label em botões que contêm apenas ícones.
  • Associação de Labels: Garantir que todo input tenha um label visual ou um aria-label descritivo.
  • Estados Dinâmicos: Usar aria-expanded, aria-pressed ou aria-hidden para comunicar mudanças de estado na interface (ex: menus que abrem/fecham).
  • Exemplo
<button type="button" class="btn-close" aria-label="Fechar modal" (click)="close()"></button>

<button 
  class="btn dropdown-toggle" 
  [attr.aria-expanded]="isOpen" 
  (click)="toggleMenu()">
  Opções
</button>

<input type="text" aria-label="Buscar usuário por nome" placeholder="Buscar...">

Respeito às Preferências (Tema e Fonte)

  • Motivação Usuários com baixa visão ou fotofobia precisam ajustar a interface para conseguir ler. Ignorar as preferências de tamanho de fonte ou impor um contraste baixo torna o sistema inutilizável para esse público.
  • Resolução
  • Utilizar o ThemeService para permitir a alternância entre temas Claro/Escuro.
  • Implementar controles de tamanho de fonte (fontSize signal) que ajustam a variável base do CSS no <html>.
  • Utilizar variáveis CSS (--theme-surface, --theme-text) em vez de cores hexadecimais fixas (#fff, #000) para garantir que o contraste seja mantido em qualquer tema.
  • Exemplo
// Componente de Controle de Acessibilidade
@Component({
  selector: 'app-accessibility-controls',
  standalone: true,
  template: `
    <button (click)="theme.toggleTheme()" aria-label="Alternar tema">
      <i class="icon-contrast"></i>
    </button>

    <button (click)="theme.adjustFontSize('increase')" aria-label="Aumentar fonte">
      A+
    </button>
  `
})
export class AccessibilityControlsComponent {
  // Injeção do serviço global de temas do projeto
  protected theme = inject(ThemeService);
}

11. Experiência do Desenvolvedor - DevEx

Padronização de Ambiente (Node e Envs)

  • Motivação Nada é mais frustrante para um desenvolvedor novo do que clonar o projeto e ele não rodar porque a versão do Node é diferente ou porque faltam variáveis de ambiente. "Funciona na minha máquina" é um sintoma de falta de padronização, o que gera perda de tempo e estresse na equipe.
  • Resolução
  • Versionamento do Node: Utilizar um arquivo .nvmrc na raiz do projeto para fixar a versão do Node.js utilizada.
  • Variáveis de Ambiente: Utilizar a biblioteca dotenv para gerenciar configurações sensíveis. O arquivo .env.example deve ser commitado para servir de modelo, enquanto o .env real é ignorado pelo Git.
  • Setup Automatizado: Manter scripts no package.json que facilitem a configuração inicial.
  • Exemplo
# Arquivo .nvmrc na raiz do projeto
v20.11.0

# Arquivo .env.example (Modelo seguro)
API_URL=http://localhost:3000/api
DEBUG_MODE=true

# Script de setup no package.json
"scripts": {
  "env:init": "cp .env.example .env",
  "start": "ng serve",
  "prepare": "husky install" // Hooks de git
}

Automação de Tarefas Repetitivas

  • Motivação Tarefas manuais como formatar código, ordenar imports ou garantir que o build não quebrou antes de subir consomem energia mental desnecessária. A automação libera o desenvolvedor para focar na lógica de negócio e garante que o padrão de qualidade seja mantido sem esforço consciente.
  • Resolução
  • Lint e Format: Configurar scripts de lint:fix e format (Prettier) que rodem automaticamente via Husky (pre-commit) ou manualmente.
  • Geradores de Código: Utilizar os esquemáticos do Angular CLI (ng generate) para criar componentes, services e pages já com a estrutura de pastas correta do projeto.
  • Atalhos (Shortcuts): Criar alias no package.json para combos de comandos frequentes.
  • Exemplo
// src/app/api/users/mocks/user.mock.ts
import { UserResponse } from '../user.types';

export const USER_MOCK: UserResponse[] = [
  { uuid: '1', display_name: 'Admin', user_email: 'admin@mith.com' },
  { uuid: '2', display_name: 'Dev', user_email: 'dev@mith.com' }
];

// Uso temporário na Service (enquanto a API não fica pronta)
findAll(): Observable<UserResponse[]> {
  // Flag de ambiente controla se usa mock ou real
  if (environment.useMocks) {
    return of(USER_MOCK).pipe(delay(500)); // Simula latência
  }
  return this.http.get<UserResponse[]>(`${this.baseUrl}/users`);
}

Configuração Obrigatória de TypeScript (Strict mode)

  • Motivação: A qualidade do código começa na configuração do compilador. Permitir verificações relaxadas reduz a efetividade da tipagem estática, enfraquece refatorações e possibilita erros silenciosos em tempo de execução. O modo strict garante maior segurança, previsibilidade e confiabilidade estrutural da aplicação. Sem ele, regras como “proibir any” tornam-se frágeis e dependentes apenas de convenção.
  • Resolução
  • O projeto deve obrigatoriamente utilizar strict: true no tsconfig.json.
  • Exemplo, as seguintes opções devem estar habilitadas:
{
  "compilerOptions": {
    "strict": true,
    "noImplicitOverride": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true
  }
}

12. Monitoramento e Observabilidade (Renan)

  • Objetivo: garantir visibilidade sobre erros, performance e comportamento do usuário em produção.

  • Tratamento Centralizado de Erros

  • Motivação: Erros tratados de forma distribuída dificultam diagnóstico, aumentam duplicação de código e podem expor informações técnicas ao usuário final.

  • Resolução:
  • Centralizar captura de erros de API via HttpInterceptor.
  • Padronizar exibição de mensagens usando ToastService ou Snackbar.
  • Mensagens técnicas devem ser registradas, não exibidas.
@Injectable()
export class SessionInterceptor implements HttpInterceptor {
  constructor(private messageService: NbMessagesService) {}

  intercept(req: HttpRequest<unknown>, next: HttpHandler) {
    return next.handle(req).pipe(
      catchError(error => {
        this.messageService.error('Ocorreu um erro. Tente novamente.');
        return throwError(() => error);
      }),
    );
  }
}
  1. Logs Estruturados

  2. Motivação: Logs não padronizados dificultam análise, correlação de eventos e integração com ferramentas externas.

  3. Resolução:
  4. Utilizar um LoggerService centralizado.
  5. Classificar logs por nível: info, warn, error.
  6. Nunca registrar dados sensíveis (tokens, dados pessoais).

Exemplo: Enviando logs para o serviço centralizado.

this.loggerService.error('Falha ao carregar os usuários', {
  status: error.status,
});
  1. Monitoramento de Performance

  2. Motivação: Problemas de performance impactam diretamente a experiência do usuário e métricas de negócio.

  3. Resolução:
  4. Monitorar tempo de resposta de APIs e carregamento de rotas.
  5. Utilizar Web Vitals e ferramentas como Lighthouse CI.
  6. Analisar bundle size e aplicar lazy loading.

Exemplo: Marcar carregando da rota para utilização do performance do DevTools do navegador.

this.router.events.subscribe(event => {
  if (event instanceof NavigationEnd) {
    performance.mark('rota-carregada');
  }
});
  1. Rastreamento de Sessão

  2. Motivação: Sem rastreamento de eventos críticos, falhas de usabilidade e erros intermitentes se tornam difíceis de reproduzir.

  3. Resolução:
  4. Registrar eventos críticos (login, falha de formulário, timeout).
  5. Não capturar dados pessoais ou sensíveis.
  6. Utilizar identificadores anônimos de sessão.

Exemplo: Serviço com integração a alguma plataforma de analytics

this.analytics.track('login_failed', {
  reason: 'credenciais_invalidas'
});
  1. Integração com Ferramentas de Observabilidade

  2. Motivação: Apenas logs locais não oferecem visibilidade suficiente em produção.

  3. Resolução:
  4. Integrar ferramentas como Sentry, LogRocket ou Datadog.
  5. Centralizar erros, métricas de performance e eventos de uso.
  6. Garantir anonimização de dados.
import * as Sentry from '@sentry/angular';
import { HttpClient } from '@angular/common/http';

constructor(private http: HttpClient) {}

chamadaApi(): void {
  this.http.get('https://api.exemplo.com/data')
    .subscribe({
      next: (data) => { console.log(data); },
      error: (err) => {
        Sentry.captureException(err);
      }
    });
}
  1. Testes e Simulações

  2. Motivação: Cenários de falha não testados tendem a causar comportamento inesperado em produção.

  3. Resolução:
  4. Simular falhas de rede e API durante testes.
  5. Garantir que UI degrade de forma elegante.
  6. Evitar travamento da aplicação em erros inesperados.
describe('Testes de usuários', () => {
  it('Deve simular erro 500', () => {
    cy.intercept('GET', '/api/usuarios', { statusCode: 500 });
    cy.visit('/usuarios');
    cy.contains('Erro ao carregar os usuarios').should('be.visible');
  });
});