Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 15 — Cliente HTTP Moderno com HttpInterceptorFn e Gestão de Erros

Este exemplo apresenta a configuração corporativa do HttpClient no Angular 18+, utilizando Functional Interceptors (HttpInterceptorFn) para injeção de tokens JWT Bearer e tratamento resiliente de erros HTTP com retentativas controladas e captura de falhas 401/500.

1. Interceptors Funcionais: src/app/interceptors/auth.interceptor.ts

// src/app/interceptors/auth.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';

// 1. Interceptor de Autenticação Funcional
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('auth_access_token');
  const correlationId = `req-${Date.now()}-${Math.floor(Math.random() * 1000)}`;

  // Clona a requisição e anexa cabeçalhos imutáveis
  const modifiedReq = req.clone({
    setHeaders: {
      'X-Correlation-ID': correlationId,
      ...(token ? { Authorization: `Bearer ${token}` } : {})
    }
  });

  return next(modifiedReq);
};

// 2. Interceptor Central de Tratamento de Erros Funcional
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        console.warn('[HTTP 401] Token expirado ou inválido. Expulsando para /login.');
        localStorage.removeItem('auth_access_token');
        router.navigate(['/login']);
      } else if (error.status === 403) {
        console.error('[HTTP 403] Permissão negada pelo servidor para o recurso:', req.url);
      } else if (error.status >= 500) {
        console.error('[HTTP 500] Erro interno no cluster de backend:', error.message);
      }

      return throwError(() => error);
    })
  );
};

2. Configuração no Bootstrap: src/main.ts

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor, errorInterceptor } from './app/interceptors/auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    // Registro de interceptores funcionais modernos na esteira do HttpClient
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor, errorInterceptor])
    )
  ]
});

3. Componente de Consumo: src/app/components/api-catalog.component.ts

// src/app/components/api-catalog.component.ts
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CommonModule } from '@angular/common';

interface ServiceEndpoint {
  id: string;
  name: string;
  version: string;
  health: 'UP' | 'DOWN';
}

@Component({
  selector: 'app-api-catalog',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="max-w-xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-4">
      <div class="flex items-center justify-between border-b pb-3">
        <div>
          <h2 class="text-xl font-bold text-slate-800">🌐 Consumo com HttpInterceptorFn</h2>
          <p class="text-xs text-slate-500">Injeção de Bearer Token e X-Correlation-ID transparente</p>
        </div>
        <button
          type="button"
          (click)="fetchServices()"
          [disabled]="isLoading()"
          class="px-3.5 py-1.5 text-xs font-semibold bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors disabled:opacity-50"
        >
          {{ isLoading() ? 'Consultando...' : 'Consultar API' }}
        </button>
      </div>

      @if (errorMessage()) {
        <div class="p-3 bg-red-50 border border-red-200 text-red-700 text-xs rounded-xl">
          ⚠️ {{ errorMessage() }}
        </div>
      }

      <!-- Lista de Serviços Retornados -->
      <div class="space-y-2">
        @for (srv of services(); track srv.id) {
          <div class="p-3 border rounded-xl flex justify-between items-center bg-slate-50 text-xs">
            <div>
              <span class="font-bold text-slate-800">{{ srv.name }}</span>
              <p class="text-slate-400 font-mono text-[10px]">Versão: {{ srv.version }}</p>
            </div>
            <span
              class="px-2 py-0.5 rounded text-[10px] font-bold"
              [class.bg-emerald-100]="srv.health === 'UP'"
              [class.text-emerald-800]="srv.health === 'UP'"
            >
              {{ srv.health }}
            </span>
          </div>
        } @empty {
          <div class="p-6 text-center text-xs text-slate-400 border border-dashed rounded-xl">
            Clique em "Consultar API" para disparar a requisição com os interceptors ativos.
          </div>
        }
      </div>
    </div>
  `
})
export class ApiCatalogComponent {
  private http = inject(HttpClient);

  services = signal<ServiceEndpoint[]>([]);
  isLoading = signal<boolean>(false);
  errorMessage = signal<string | null>(null);

  fetchServices(): void {
    this.isLoading.set(true);
    this.errorMessage.set(null);

    // O authInterceptor injetará os cabeçalhos Authorization e X-Correlation-ID automaticamente
    this.http
      .get<ServiceEndpoint[]>('https://api.empresa.com.br/v1/services')
      .subscribe({
        next: (data) => {
          this.services.set(data);
          this.isLoading.set(false);
        },
        error: (err) => {
          this.errorMessage.set(`Falha na comunicação: ${err.message}`);
          this.isLoading.set(false);
        }
      });
  }
}

🖥️ Saída / Comportamento Esperado

Inspeção de Cabeçalhos HTTP no DevTools (Network Tab):
GET https://api.empresa.com.br/v1/services HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsIn...
X-Correlation-ID: req-1727489200-482

Caso a API responda com status HTTP 401 (Unauthorized):
errorInterceptor intercepta a falha:
Console: "[HTTP 401] Token expirado ou inválido. Expulsando para /login."
localStorage é limpo e o usuário é redirecionado para a tela de autenticação.

🔍 Destaques da Implementação

  1. HttpInterceptorFn: Funções puras sem estado substituem a interface arcaica HttpInterceptor com classes e injeções múltiplas HTTP_INTERCEPTORS.
  2. withInterceptors([fn1, fn2]): Execução ordenada previsível da esteira de requisição e resposta.
  3. Imutabilidade com req.clone(): Garante que a requisição original nunca seja mutada acidentalmente em caso de retentativas.