Entendendo os Interceptors funcionais no Angular
O Angular é um framework muito robusto que oferece uma ampla gama de recursos para o desenvolvimento de aplicativos web escaláveis e eficientes. Entre esses recursos, os interceptors se destacam como uma ferramenta poderosa para manipular requisições HTTP de forma centralizada e eficiente.
O que são os Interceptors?
Em termos simples, um Interceptor no Angular é um pattern que nos permite interceptar, tratar e gerenciar requisições HTTP, antes ou depois delas serem enviadas ao servidor. Eles atuam como uma espécie de middleware entre o cliente e o servidor, permitindo a execução de lógica adicional em várias etapas do ciclo de vida de uma requisição HTTP.
Casos de Uso Comuns
Os interceptors podem ser utilizados para uma variedade de tarefas comuns em aplicações Angular, incluindo:
- Autenticação: Adicionar tokens de autenticação ou headers de autorização a requisições HTTP.
- Logging: Registrar informações sobre as requisições e respostas para fins de debug ou monitoramento.
- Cache: Implementar estratégias de cache para reduzir o número de requisições repetidas.
- Manipulação de erros: Interceptando respostas HTTP para tratar erros de forma centralizada.
- Transformação de requisições e respostas: Modificar dados antes de serem enviados ou depois de serem recebidos pelo servidor.
Interceptor como função
Antes da versão 14 do Angular, a única que forma que tínhamos de criar Interceptors era por meio de classes injetáveis, que nada mais eram do que uma Service. Veja um exemplo de Interceptor em forma de classe:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request);
}
}
Embora esta ainda seja uma forma válida de criar Interceptors, o Angular recomenda fortemente para utilizá-los em forma de funções. Se você está por dentro das mudanças e novidades das últimas versões, já deve ter percebido que o Angular está migrando para abordagens mais simples, como os Standalone Components e a mudança de outros recursos como guards e resolvers para uma abordagem funcional também.
Isso significa que para criar um Interceptor agora, basta criarmos uma função. Veja o mesmo exemplo anterior que foi escrito em forma de classe, agora como função:
export const authInterceptor: HttpInterceptorFn = (request, next) =>
return next.handle(request);
Bem mais simples, certo? E para utilizá-lo em nossa aplicação também ficou mais simples.
Se você já trabalha com Standalone Components, pode ter percebido que não temos mais um AppModule. Ele deu lugar a um objeto de configuração alocado em app.config.ts. É neste objeto que vamos importar nosso Interceptor e quantos outros forem necessários.
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor])
// passamos um array de interceptors
),
],
};
Utilização e Funcionamento
Afim de entender suas funcionalidades, vamos criar um Interceptor funcional com dois casos de uso muito comuns: adicionar um token no headers da requisição e exibir uma mensagem de erro genérica.
export const httpAuthErrorsInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const snackBar = inject(MatSnackBar);
const clonedRequest = req.clone({
setHeaders: {
'x-access-token': this.authService.getToken(),
},
});
return next(clonedRequest).pipe(
catchError((error) => {
snackBar.open('Ops, houve um erro', 'Fechar', {
duration: 5000,
});
return throwError(() => error);
})
);
};
O primeiro caso é o do token. Neste exemplo, o interceptor adiciona um cabeçalho de autorização às requisições HTTP, utilizando um token de autenticação obtido do serviço authService
.
O segundo caso é o da manipulação de erros. Ao ocorrer um erro em qualquer requisição, exibimos um alert para o usuário com a seguinte mensagem: ‘Ops, houve um erro’.
Importante: Como esse interceptor possui duas responsabilidades, o ideal seria criar um interceptor para cada caso de uso. Este foi somente um exemplo para facilitar a demonstração.
Testando um Interceptor
Para criar testes automatizados de um interceptor, precisamos primeiro pensar na sua funcionalidade e quando ele será executado. Sabemos que ele entrará em ação a cada requisição HTTP, e temos uma mensagem de erro a cada tentativa falha.
Portanto, no teste automatizado, precisamos simular uma requisição HTTP que ocorre um erro. Desta forma conseguimos verificar se a mensagem de erro foi de fato exibida nestas situações.
Setup do teste
Felizmente, o Angular já nos oferece várias ferramentas que auxiliam em testes utilizando requisições HTTP. Primeiramente, precisamos criar o setup inicial para os testes.
describe('httpAuthErrorsInterceptor', () => {
let httpMock: HttpTestingController;
let httpClient: HttpClient;
let snackBar: MatSnackBar;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([httpAuthErrorsInterceptor])),
provideHttpClientTesting(),
],
});
httpMock = TestBed.inject(HttpTestingController);
httpClient = TestBed.inject(HttpClient);
snackBar = TestBed.inject(MatSnackBar);
});
...
Precisamos dizer ao teste que nosso interceptor deve entrar em ação, da mesma forma que fizemos ao importá-lo em app.config.ts.
Também injetamos três dependências:
HttpTestingController
: classe nativa do Angular responsável por mockar requisições HTTP.HttpClient
: Service para executarmos uma requisição qualquer, afim de verificar o funcionamento do interceptor.MatSnackBar
: A mesma Service que injetamos no Interceptor, para espionarmos se a mensagem foi chamada corretamente ao ocorrer um erro.
Escrevendo o teste automatizado
Agora vamos ao teste de fato. Ele deve testar os dois casos de uso: o caso do token e o caso da mensagem de erro.
it('should set token and open notification on http error', () => {
jest.spyOn(snackBar, 'open');
httpClient.get('/test').subscribe();
const request = httpMock.expectOne('/test');
request.error(new ProgressEvent('error'));
expect(snackBar.open).toHaveBeenCalled();
expect(request.request.headers.has('x-access-token')).toBe(true);
});
Primeiramente é necessário espionar o método open
da classe MatSnackBar
. Aqui estou utilizando o Jest, mas seria bastante semelhante caso esteja utilizando o Jasmine.
Agora entra a questão que havia comentado anteriormente: precisamos realizar uma requisição qualquer e simular que a mesma contenha um erro. Perceba que o endpoint da requisição pouco importa. O importante é realizar para que o Interceptor entre em ação.
Com a requisição realizada com erro, basta fazermos nossas asserções: verificamos se o método snackBar.open
foi de fato chamado e por fim testamos se o header da requisição possui a chave x-access-token
.
Conclusão
Os interceptors no Angular são uma ferramenta extremamente útil e versátil para manipulação de requisições HTTP em aplicações web. Ao centralizar a lógica de manipulação de requisições e respostas, os interceptors ajudam a manter um código limpo, organizado e fácil de manter.
Entendemos também o funcionamento dos interceptors em forma de função, que substitui a abordagem antiga de classes. Além disso vimos como podemos criar testes automatizados para eles.