Experts in Angular

HTTP ClientDesvendando Interceptação em Requisições HTTP

Desvendando Interceptação em Requisições HTTP

Em nossa jornada pelo universo do HttpClient do Angular, exploramos como realizar requisições, buscar diferentes tipos de dados e lidar com falhas. Agora, vamos desvendar um dos recursos mais poderosos e versáteis dessa ferramenta: os interceptores.

Imagine os interceptores como agentes secretos que atuam nos bastidores da comunicação entre sua aplicação e o servidor. Eles têm o poder de interceptar e manipular requisições e respostas, adicionando uma camada extra de controle e flexibilidade à sua aplicação.

O que são Interceptores?

Interceptores são funções que você pode executar para cada requisição HTTP, permitindo que você realize diversas ações, como:

  • Adicionar cabeçalhos de autenticação a requisições.
  • Tentar novamente requisições que falharam.
  • Armazenar respostas em cache.
  • Personalizar a análise de respostas.
  • Medir o tempo de resposta do servidor.
  • Exibir elementos de interface do usuário, como um spinner de carregamento.
  • Coletar e agrupar requisições.
  • Cancelar requisições após um tempo limite.
  • Realizar polling do servidor para atualizar dados.

Os interceptores formam uma cadeia, onde cada um processa a requisição ou resposta antes de passá-la para o próximo interceptor na cadeia.

Tipos de Interceptores

O HttpClient suporta dois tipos de interceptores:

  • Interceptores Funcionais: Funções que recebem a requisição e um manipulador, e retornam um Observable da resposta. São mais simples e flexíveis, e nossa recomendação é utilizá-los.
  • Interceptores Baseados em DI (Injeção de Dependência): Classes que implementam a interface HttpInterceptor. Embora ainda suportados, eles possuem um comportamento menos previsível, especialmente em configurações complexas.

Neste artigo, vamos focar nos interceptores funcionais, explorando como defini-los, configurá-los e utilizá-los para criar aplicações Angular mais poderosas e eficientes.

Definindo um Interceptor: A Primeira Linha de Defesa

Agora que entendemos o conceito de interceptores, vamos criar nosso primeiro agente secreto. A forma básica de um interceptor é uma função que recebe a requisição HTTP de saída (HttpRequest) e uma função next, que representa o próximo passo no processamento da cadeia de interceptores.

Exemplo: Interceptor de Registro

Vamos criar um interceptor simples que registra a URL da requisição no console antes de encaminhá-la:

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  console.log(req.url);
  return next(req);
}

Neste exemplo, a função loggingInterceptor recebe a requisição (req) e a função next. Ela registra a URL da requisição no console usando console.log(req.url) e, em seguida, chama a função next(req) para passar a requisição adiante na cadeia de interceptores.

Configurando Interceptores: Montando a Cadeia de Defesa

Agora que sabemos como criar interceptores, vamos aprender a configurá-los para que eles possam proteger e aprimorar nossas requisições HTTP.

Configuração via Injeção de Dependências

A maneira mais comum de configurar interceptores é através da função withInterceptors, que faz parte da configuração do HttpClient usando a injeção de dependências:

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([loggingInterceptor, cachingInterceptor])
    )
  ]
});

Neste exemplo, estamos configurando dois interceptores: loggingInterceptor e cachingInterceptor. A ordem em que eles são listados é importante, pois define a ordem de execução na cadeia de interceptores. No nosso caso, o loggingInterceptor será executado primeiro, seguido pelo cachingInterceptor.

Fluxo da Cadeia de Interceptores

  1. Requisição: Quando uma requisição é feita pelo HttpClient, ela entra na cadeia de interceptores.
  2. Primeiro Interceptor: O primeiro interceptor (loggingInterceptor) recebe a requisição e a função next. Ele pode realizar ações como registrar informações da requisição, modificar o cabeçalho ou até mesmo cancelar a requisição.
  3. Próximo Interceptor: O loggingInterceptor chama a função next para passar a requisição (possivelmente modificada) para o próximo interceptor na cadeia (cachingInterceptor).
  4. Segundo Interceptor: O cachingInterceptor recebe a requisição e a função next. Ele pode verificar se a resposta está em cache, retornar a resposta do cache ou permitir que a requisição continue para o servidor.
  5. Servidor (Opcional): Se a requisição não for interrompida ou respondida pelo cache, ela é enviada ao servidor.
  6. Resposta: A resposta do servidor retorna pela cadeia de interceptores, na ordem inversa. Cada interceptor tem a oportunidade de examinar e modificar a resposta antes de passá-la para o interceptor anterior.
  7. Resultado: O Observable da resposta (possivelmente modificada) é retornado para o componente ou serviço que fez a requisição.

Flexibilidade e Reutilização

A configuração de interceptores através da injeção de dependências oferece grande flexibilidade. Você pode configurar diferentes conjuntos de interceptores para diferentes partes da sua aplicação, ou até mesmo criar interceptores dinâmicos que são adicionados ou removidos da cadeia em tempo de execução.

Além disso, os interceptores são altamente reutilizáveis. Você pode criar um conjunto de interceptores genéricos que resolvem problemas comuns, como autenticação, cache ou tratamento de erros, e reutilizá-los em diferentes projetos.

Com a configuração adequada dos interceptores, você pode transformar o HttpClient em uma ferramenta ainda mais poderosa, adicionando funcionalidades personalizadas e otimizando a comunicação da sua aplicação Angular com o servidor.

Interceptando Eventos de Resposta: Uma Visão Privilegiada da Comunicação

Além de manipular requisições, os interceptores também podem interceptar e transformar o fluxo de eventos da resposta do servidor. Isso nos permite ter acesso a informações valiosas, como o status da resposta, os cabeçalhos e o progresso do download, e realizar ações personalizadas em cada etapa do processo.

Inspecionando o Tipo de Evento

O fluxo de eventos retornado pelo HttpClient é um Observable de HttpEvent. Cada HttpEvent possui um atributo type que indica o tipo de evento, como HttpEventType.Sent (requisição enviada), HttpEventType.ResponseHeader (cabeçalho da resposta recebido) e HttpEventType.Response (resposta completa recebida).

Para identificar o evento que representa a resposta completa, podemos verificar o valor de event.type.

Exemplo: Interceptor de Registro de Resposta

Vamos aprimorar nosso interceptor de registro para que ele também registre o status da resposta no console:

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  return next(req).pipe(
    tap(event => {
      if (event.type === HttpEventType.Response) {
        console.log(req.url, 'retornou uma resposta com status', event.status);
      }
    })
  );
}

Neste exemplo, utilizamos o operador tap do RxJS para interceptar cada evento do fluxo. Quando o evento for do tipo HttpEventType.Response, registramos a URL da requisição e o status da resposta no console.

Associação Natural de Requisições e Respostas

Um dos benefícios dos interceptores é que eles associam naturalmente as respostas às suas requisições correspondentes. Isso ocorre porque a transformação do fluxo de resposta é feita em um closure que captura o objeto da requisição.

Com essa associação, você pode facilmente relacionar informações da resposta, como o status, com a requisição original, facilitando a análise e o tratamento de erros.

Monitorando o Progresso do Download

Além do status da resposta, você também pode interceptar eventos de progresso do download (HttpEventType.DownloadProgress) para exibir uma barra de progresso ou realizar outras ações personalizadas.

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  return next(req).pipe(
    tap(event => {
      if (event.type === HttpEventType.DownloadProgress) {
        const percentDone = Math.round(100 * event.loaded / event.total);
        console.log(`Download em progresso: ${percentDone}%`);
      } else if (event.type === HttpEventType.Response) {
        console.log(req.url, 'retornou uma resposta com status', event.status);
      }
    })
  );
}

Ao interceptar eventos de resposta, você ganha um controle granular sobre a comunicação com o servidor, permitindo que sua aplicação reaja de forma personalizada a cada etapa do processo.

Modificando Requisições: A Arte da Transformação Imutável

Interceptores não se limitam apenas a observar as requisições e respostas. Eles também podem modificá-las para adicionar funcionalidades como autenticação, cache ou manipulação de dados. No entanto, a maneira como essa modificação é feita é crucial para garantir a segurança e a consistência da sua aplicação.

A Imutalibidade do HttpRequest e HttpResponse

A maioria dos aspectos das instâncias de HttpRequest e HttpResponse são imutáveis, o que significa que você não pode alterá-los diretamente. Essa imutabilidade é importante para garantir que os interceptores não interfiram uns nos outros e que as requisições e respostas sejam processadas de forma consistente.

Clonagem: A Chave para a Modificação

Para modificar uma requisição ou resposta, você deve primeiro cloná-la usando o método .clone(). A clonagem cria uma nova instância do objeto com as mesmas propriedades da instância original. Ao clonar, você pode especificar quais propriedades devem ser modificadas na nova instância.

Exemplo: Adicionando um Cabeçalho

const reqWithHeader = req.clone({
  headers: req.headers.set('X-New-Header', 'new header value'),
});

Neste exemplo, clonamos a requisição original (req) e adicionamos um novo cabeçalho (X-New-Header) à nova instância (reqWithHeader). Observe que o método set também retorna uma nova instância de HttpHeaders, mantendo a imutabilidade.

Idempotência: A Segurança da Imutabilidade

A imutabilidade dos objetos HttpRequest e HttpResponse permite que a maioria dos interceptores sejam idempotentes. Isso significa que, se a mesma requisição for enviada para a cadeia de interceptores várias vezes, o resultado será o mesmo em todas as execuções.

Essa característica é importante em cenários como a repetição de requisições após uma falha, garantindo que a requisição original não seja modificada acidentalmente.

Atenção ao Corpo da Requisição/Resposta

O corpo da requisição ou resposta (body) não é protegido contra mutações profundas. Se um interceptor precisar modificar o corpo, tome cuidado para lidar com a possibilidade de executar várias vezes na mesma requisição, pois as alterações podem se acumular.

Ao dominar a arte da modificação imutável, você garante que seus interceptores sejam seguros, eficientes e reutilizáveis, adicionando uma camada extra de poder e flexibilidade à sua aplicação Angular.

Exemplo de Modificação de Requisição

Vamos definir um interceptor que adiciona um cabeçalho de autenticação a todas as requisições de saída:

import { HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  const authReq = req.clone({
    setHeaders: { Authorization: 'Bearer my-auth-token' }
  });
  return next(authReq);
}

Modificando Parâmetros de URL

Da mesma forma, você pode modificar os parâmetros de URL clonando a requisição e ajustando os parâmetros:

const reqWithParams = req.clone({
  params: req.params.set('param1', 'value1').set('param2', 'value2'),
});

Modificar requisições no HttpClient do Angular usando interceptores é uma maneira eficaz de adicionar cabeçalhos, ajustar parâmetros de URL e fazer outras mudanças necessárias nas requisições de saída. Usando a operação .clone(), você pode garantir que essas modificações sejam feitas de forma imutável, preservando a idempotência e a consistência das requisições.

Injeção de Dependência em Interceptores: Acessando o Arsenal da Aplicação

Os interceptores não estão isolados do resto da sua aplicação. Eles podem se beneficiar do poderoso mecanismo de injeção de dependência (DI) do Angular para acessar serviços e outros recursos necessários para realizar suas tarefas.

Exemplo: Interceptor de Autenticação

Imagine que sua aplicação possui um serviço chamado AuthService, responsável por gerenciar tokens de autenticação. Um interceptor pode injetar esse serviço para obter um token e adicioná-lo ao cabeçalho de cada requisição.

export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  const authService = inject(AuthService); // Injetar o serviço AuthService
  const authToken = authService.getAuthToken(); // Obter o token de autenticação

  const newReq = req.clone({
    headers: req.headers.append('X-Authentication-Token', authToken) // Adicionar o token ao cabeçalho
  });

  return next(newReq); // Enviar a requisição modificada
}

Neste exemplo, a função inject do Angular é utilizada para obter uma instância do serviço AuthService. Em seguida, o interceptor obtém o token de autenticação do serviço e o adiciona ao cabeçalho da requisição antes de encaminhá-la.

Contexto de Injeção

Os interceptores são executados no contexto de injeção do injetor que os registrou. Isso significa que eles têm acesso aos mesmos provedores de serviços que o componente ou serviço que fez a requisição.

Flexibilidade e Modularidade

A injeção de dependência em interceptores permite que você crie interceptores mais flexíveis e modulares. Você pode injetar diferentes serviços em diferentes interceptores, de acordo com suas necessidades. Isso facilita a organização do código e a reutilização de interceptores em diferentes partes da aplicação.

Com o poder da injeção de dependência, os interceptores se tornam verdadeiros agentes secretos, equipados com todo o arsenal da sua aplicação para realizar suas missões de forma eficiente e eficaz.

Metadados de Requisição e Resposta: Comunicação Secreta entre Interceptores

Em algumas situações, é útil incluir informações em uma requisição que não são enviadas ao servidor, mas são destinadas especificamente aos interceptores. Para isso, o HttpClient oferece um mecanismo de metadados que permite armazenar e compartilhar informações entre os interceptores de forma segura e eficiente.

HttpContext: O Cofre de Metadados

As requisições HTTP possuem um objeto .context que armazena esses metadados como uma instância de HttpContext. Esse objeto funciona como um mapa tipado, com chaves do tipo HttpContextToken e valores de qualquer tipo.

Imagine o HttpContext como um cofre secreto onde os interceptores podem guardar e recuperar informações importantes para o processamento da requisição. Cada interceptor pode acessar o cofre, ler as informações deixadas por outros interceptores e adicionar suas próprias informações, criando um fluxo de comunicação eficiente e transparente.

Definindo Tokens de Contexto

Para armazenar informações no HttpContext, você precisa definir um token de contexto, que é um objeto do tipo HttpContextToken. O token serve como uma chave única para identificar o valor armazenado no mapa.

export const CACHING_ENABLED = new HttpContextToken<boolean>(() => true);

Neste exemplo, criamos um token chamado CACHING_ENABLED, que armazena um valor booleano indicando se o cache está habilitado para a requisição. A função fornecida ao construtor define o valor padrão do token para requisições que não o definiram explicitamente.

Lendo e Configurando Tokens de Contexto: Comunicação Precisa entre Interceptores

Após definirmos os tokens de contexto, nossos interceptores podem ler e utilizar essas informações para tomar decisões inteligentes sobre como processar a requisição.

Lendo o Token em um Interceptor

No interceptor, você pode utilizar o método req.context.get(TOKEN) para obter o valor de um token de contexto específico.

export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  if (req.context.get(CACHING_ENABLED)) {
    // aplicar lógica de cache
    return ...;
  } else {
    // o cache foi desabilitado para esta requisição
    return next(req);
  }
}

Neste exemplo, o interceptor cachingInterceptor verifica se o cache está habilitado para a requisição lendo o valor do token CACHING_ENABLED. Se o cache estiver habilitado, o interceptor aplica a lógica de cache. Caso contrário, ele simplesmente passa a requisição adiante na cadeia.

Configurando Tokens de Contexto ao Fazer uma Requisição

Você pode definir valores para os tokens de contexto ao fazer uma requisição usando o HttpClient. Para isso, crie uma instância de HttpContext e utilize o método set(TOKEN, valor) para definir o valor do token.

const data$ = http.get('/sensitive/data', {
  context: new HttpContext().set(CACHING_ENABLED, false),
});

Neste exemplo, estamos desabilitando o cache para a requisição /sensitive/data ao definir o valor do token CACHING_ENABLED como false. Os interceptores podem então ler esse valor no objeto HttpContext da requisição.

O Contexto da Requisição é Mutável

O objeto HttpContext é mutável, o que significa que você pode adicionar, remover ou modificar valores dos tokens de contexto em qualquer ponto da cadeia de interceptores. Isso permite que os interceptores se comuniquem de forma dinâmica e flexível, adaptando o comportamento da requisição conforme necessário.

Com o poder dos metadados e tokens de contexto, você pode criar interceptores mais inteligentes e personalizados, que se adaptam às necessidades específicas da sua aplicação Angular.

Vantagens dos Tokens de Contexto

  • Tipagem: Os tokens de contexto garantem que os valores armazenados no HttpContext sejam do tipo correto, evitando erros em tempo de execução.
  • Organização: Os tokens de contexto facilitam a organização dos metadados, separando-os em categorias lógicas e tornando o código mais fácil de entender.
  • Reutilização: Os tokens de contexto podem ser reutilizados em diferentes interceptores, promovendo a reutilização de código e a modularidade.

Com os tokens de contexto e o HttpContext, você tem à sua disposição uma ferramenta poderosa para compartilhar informações entre interceptores, adicionando uma camada extra de flexibilidade e controle à sua aplicação Angular.

Respostas Sintéticas: Criando Respostas Personalizadas sem o Servidor

Em algumas situações, você pode querer que um interceptor responda à requisição sem precisar enviá-la ao servidor. Isso pode ser útil para criar respostas em cache, simular erros ou fornecer dados temporários enquanto a requisição real está em andamento. Para isso, o HttpClient permite que você crie respostas sintéticas, ou seja, respostas construídas pelo próprio interceptor.

O Poder do HttpResponse

A classe HttpResponse oferece um construtor que permite criar respostas HTTP personalizadas, definindo o status, os cabeçalhos e o corpo da resposta.

const resp = new HttpResponse({
  body: 'corpo da resposta',
  status: 200, // Opcional, o padrão é 200
  headers: new HttpHeaders({ 'Content-Type': 'text/plain' }) // Opcional
});

Neste exemplo, criamos uma resposta sintética com o corpo “corpo da resposta”, status 200 e cabeçalho Content-Type definido como text/plain. Você pode personalizar esses valores de acordo com suas necessidades.

Retornando a Resposta Sintética

Para retornar a resposta sintética ao invés de enviar a requisição ao servidor, basta retornar um Observable da resposta no interceptor.

export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  const cachedResponse = getCachedResponse(req.url); // Obter a resposta do cache

  if (cachedResponse) {
    return of(cachedResponse); // Retornar a resposta do cache
  }

  return next(req); // Enviar a requisição ao servidor
}

Neste exemplo, o interceptor cachingInterceptor primeiro verifica se existe uma resposta em cache para a requisição. Se existir, ele retorna um Observable da resposta em cache usando a função of do RxJS. Caso contrário, ele passa a requisição adiante na cadeia.

Usos das Respostas Sintéticas

As respostas sintéticas abrem um leque de possibilidades para criar interceptores mais inteligentes e eficientes. Você pode usá-las para:

  • Cache: Armazenar respostas em cache e retorná-las em requisições subsequentes, reduzindo a carga no servidor e melhorando o desempenho da aplicação.
  • Simulação de Erros: Simular respostas de erro para testar o comportamento da sua aplicação em diferentes cenários.
  • Dados Temporários: Fornecer dados temporários enquanto a requisição real está em andamento, melhorando a experiência do usuário.

Ao dominar a arte das respostas sintéticas, você adiciona uma camada extra de flexibilidade e controle à sua aplicação Angular, permitindo que você personalize o comportamento das requisições HTTP de acordo com suas necessidades.

Interceptores Baseados em DI: Uma Abordagem Alternativa

Além dos interceptores funcionais, o HttpClient também suporta interceptores definidos como classes injetáveis através do sistema de injeção de dependência (DI). As capacidades desses interceptores baseados em DI são idênticas às dos interceptores funcionais, mas o mecanismo de configuração é diferente.

Definindo um Interceptor Baseado em DI

Um interceptor baseado em DI é uma classe injetável que implementa a interface HttpInterceptor:

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
    console.log('URL da Requisição:', req.url);
    return handler.handle(req);
  }
}

Neste exemplo, a classe LoggingInterceptor implementa o método intercept, que recebe a requisição (req) e um manipulador (handler). O interceptor registra a URL da requisição no console e, em seguida, chama o método handler.handle(req) para passar a requisição adiante na cadeia.

Configurando Interceptores Baseados em DI

Interceptores baseados em DI são configurados através de um multi-provedor de injeção de dependência:

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptorsFromDi(), // Habilitar interceptores baseados em DI
    ),
    { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
  ]
});

Neste exemplo, estamos configurando o interceptor LoggingInterceptor. A opção multi: true indica que estamos fornecendo múltiplas instâncias do token HTTP_INTERCEPTORS, permitindo que você registre vários interceptores.

Ordem de Execução

Interceptores baseados em DI são executados na ordem em que seus provedores são registrados. Em uma aplicação com uma configuração de DI extensa e hierárquica, essa ordem pode ser difícil de prever.

Comparativo: Interceptores Funcionais vs. Baseados em DI

CaracterísticaInterceptores FuncionaisInterceptores Baseados em DI
SimplicidadeMais simplesMais complexo
FlexibilidadeMais flexívelMenos flexível
Ordem de execuçãoMais previsívelMenos previsível
ReutilizaçãoMais fácilMais difícil

Em geral, recomendamos o uso de interceptores funcionais, pois eles são mais simples, flexíveis e oferecem uma ordem de execução mais previsível. No entanto, interceptores baseados em DI podem ser úteis em cenários específicos, como quando você precisa de acesso a recursos do Angular em seus interceptores, como diretivas ou pipes.

Com este guia completo sobre interceptores, você está pronto para dominar essa poderosa ferramenta e criar aplicações Angular mais robustas, eficientes e personalizadas.

Resumo da Terceira Parte: Interceptores no Angular HttpClient

Na terceira e última parte da nossa série de artigos sobre o HttpClient do Angular, mergulhamos no mundo dos interceptores, explorando como eles podem ser usados para transformar e gerenciar requisições e respostas HTTP de maneira eficiente e modular.

Interceptores: O Que São e Como Funcionam

Interceptores são middlewares poderosos que permitem modificar requisições e respostas HTTP. Eles formam uma cadeia onde cada interceptor processa a requisição ou resposta antes de encaminhá-la ao próximo. Isso permite implementar padrões comuns, como autenticação, caching, logging e retentativas de requisição.

Tipos de Interceptores

  • Funcionais: São definidos como funções e têm um comportamento mais previsível. Recomendamos o uso de interceptores funcionais para a maioria dos casos.
  • Baseados em DI: São definidos como classes injetáveis que implementam a interface HttpInterceptor. Eles são configurados através do sistema de Injeção de Dependência do Angular.

Definindo e Configurando Interceptores

  • Funcionais: Definidos como funções que recebem uma requisição e um manipulador, e retornam um Observable. São configurados usando a função provideHttpClient com withInterceptors.
  • Baseados em DI: Definidos como classes que implementam a interface HttpInterceptor. São configurados através de um multi-provedor de injeção de dependência e requerem o uso de withInterceptorsFromDi.

Interceptando e Modificando Requisições e Respostas

  • Modificando Requisições: Interceptores podem modificar cabeçalhos, parâmetros e o corpo de requisições clonando-as e aplicando as alterações necessárias.
  • Interceptando Eventos de Resposta: Interceptores podem acessar e manipular eventos de resposta, como registrar tempos de resposta ou status das requisições.

Injeção de Dependência em Interceptores

Interceptores podem usar a API inject do Angular para acessar serviços e outras dependências, tornando-os mais poderosos e flexíveis.

Metadados de Requisição e Resposta

Usando HttpContext e HttpContextToken, você pode adicionar metadados às requisições que não são enviados ao backend, mas são usados pelos interceptores para tomar decisões de processamento.

Conclusão

Em suma, os interceptores do HttpClient são verdadeiros agentes secretos que atuam nos bastidores da sua aplicação Angular, interceptando e manipulando requisições e respostas HTTP. Com eles, você pode adicionar funcionalidades como autenticação, cache, registro e muito mais, de forma modular e reutilizável.

Com este guia completo sobre interceptores, você está pronto para dominar essa poderosa ferramenta e levar suas aplicações Angular para o próximo nível.