Capítulo 15 - Parte 1: Interceptores funcionales (Angular 15+): concepto y estructura¶
Parte 1 de 4 · Capítulo 15 · PARTE VIII - Comunicación HTTP
Cuando una aplicación Angular crece, inevitablemente aparece la necesidad de hacer algo con todas las peticiones HTTP: adjuntar un token de autenticación, registrar los tiempos de respuesta, mostrar un indicador de carga, manejar errores de red de forma uniforme. Sin interceptores, esa lógica se dispersa por todos los servicios. Con interceptores, la centralizamos en un único lugar y la aplicamos automáticamente a cada petición que sale de la aplicación.
Angular 15 introdujo los interceptores funcionales, una alternativa más simple y alineada con el enfoque moderno sin NgModule. Veamos cómo funcionan.
¿Qué es un interceptor y para qué sirve?¶
Un interceptor es un mecanismo que se interpone en el flujo de comunicación HTTP entre la aplicación y el servidor. Puede inspeccionar, modificar o incluso cancelar peticiones antes de que salgan, y puede hacer lo mismo con las respuestas antes de que lleguen al código que las solicitó.
Los casos de uso más comunes son:
- Autenticación: adjuntar el JWT a cada petición que lo requiera.
- Logging: registrar la URL, el método y el tiempo de respuesta de cada llamada.
- Loading global: mostrar y ocultar un spinner centralizado.
- Manejo de errores: interceptar los errores HTTP y mostrar notificaciones al usuario.
- Transformación de datos: normalizar el formato de respuesta de la API antes de entregarlo a los servicios.
La gran ventaja frente a poner esta lógica en cada servicio es el principio DRY: un solo lugar de cambio, comportamiento consistente en toda la aplicación.
HttpInterceptorFn: la firma que debemos conocer¶
En Angular 15+, un interceptor funcional es simplemente una función con esta firma:
// firma de referencia (de @angular/common/http)
// type HttpInterceptorFn = (
// req: HttpRequest<unknown>,
// next: HttpHandlerFn
// ) => Observable<HttpEvent<unknown>>;
No hay clases, no hay interfaces que implementar, no hay @Injectable(). Solo una función. Veamos el interceptor más simple posible: uno que no hace nada y deja pasar la petición tal cual:
// interceptores/transparente.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
export const interceptorTransparente: HttpInterceptorFn = (solicitud, siguiente) => {
return siguiente(solicitud);
};
solicitud es el HttpRequest original. siguiente es la función que pasa la solicitud al próximo interceptor en la cadena (o al backend si no hay más interceptores). Siempre debemos devolver el observable que siguiente produce; de lo contrario, la petición muere en nuestro interceptor.
request.clone(): inmutabilidad como principio¶
Un HttpRequest es inmutable. No podemos modificar sus propiedades directamente. Para enviar una versión modificada al siguiente eslabón de la cadena, usamos clone():
// interceptores/ejemplo-clone.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
export const interceptorEjemplo: HttpInterceptorFn = (solicitud, siguiente) => {
const solicitudModificada = solicitud.clone({
// Podemos modificar headers, URL, params, body, etc.
headers: solicitud.headers
.set('X-Cliente-Version', '2.1.0')
.set('Accept-Language', 'es-MX'),
url: solicitud.url.replace('http://', 'https://'),
});
return siguiente(solicitudModificada);
};
clone() crea una nueva instancia de HttpRequest copiando todas las propiedades y aplicando los cambios que especificamos. Los headers de HttpHeaders también son inmutables; set() siempre devuelve una nueva instancia con el header agregado o reemplazado.
Registrar interceptores con provideHttpClient¶
La forma de registrar interceptores funcionales es a través de withInterceptors() dentro de provideHttpClient() en el app.config.ts:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { interceptorDeLog } from './interceptores/log.interceptor';
import { interceptorDeAuth } from './interceptores/auth.interceptor';
import { interceptorDeLoading } from './interceptores/loading.interceptor';
import { interceptorDeErrores } from './interceptores/errores.interceptor';
import { rutas } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(rutas),
provideHttpClient(
withInterceptors([
interceptorDeLog,
interceptorDeAuth,
interceptorDeLoading,
interceptorDeErrores,
]),
),
],
};
El orden en el array importa, y mucho.
Orden de ejecución de interceptores¶
La cadena de interceptores funciona como una pila. En la petición (saliente) se ejecutan de izquierda a derecha; en la respuesta (entrante) se ejecutan de derecha a izquierda. Visualicémoslo con el ejemplo anterior:
sequenceDiagram
participant App as Componente / Servicio
participant Log as interceptorDeLog
participant Auth as interceptorDeAuth
participant Loading as interceptorDeLoading
participant Err as interceptorDeErrores
participant API as Servidor
App->>Log: HttpRequest original
Log->>Auth: req (con log de salida)
Auth->>Loading: req (con JWT adjunto)
Loading->>Err: req (loading activo)
Err->>API: req final
API-->>Err: HttpResponse
Err-->>Loading: response (o error transformado)
Loading-->>Auth: response (loading desactivado)
Auth-->>Log: response
Log-->>App: response (con log de entrada)
Este flujo bidireccional es lo que hace a los interceptores tan poderosos: el mismo mecanismo que adjunta el token en la salida puede manejar el error 401 en la entrada.
Uso de inject() dentro de interceptores funcionales¶
Una de las ventajas más importantes de los interceptores funcionales es que podemos usar inject() directamente dentro de la función, lo que nos da acceso a cualquier servicio del árbol de inyección:
// interceptores/log.interceptor.ts
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { tap } from 'rxjs/operators';
import { LogService } from '../services/log.service';
export const interceptorDeLog: HttpInterceptorFn = (solicitud, siguiente) => {
const logService = inject(LogService);
const inicio = Date.now();
logService.debug(`→ ${solicitud.method} ${solicitud.url}`);
return siguiente(solicitud).pipe(
tap((evento) => {
if (evento instanceof HttpResponse) {
const duracion = Date.now() - inicio;
logService.debug(
`← ${evento.status} ${solicitud.url} (${duracion}ms)`,
);
}
}),
);
};
inject() se llama en el momento en que se ejecuta la función interceptora, que siempre ocurre dentro del contexto de inyección de Angular. Esto es equivalente a usar el constructor en la versión basada en clases, pero sin necesidad de decoradores.
Diagrama completo de la arquitectura de interceptores¶
flowchart LR
subgraph Aplicación
Svc[Servicios HTTP]
HC[HttpClient]
end
subgraph Cadena de Interceptores
I1[interceptorDeLog]
I2[interceptorDeAuth]
I3[interceptorDeLoading]
I4[interceptorDeErrores]
end
subgraph Backend
API[REST API]
end
Svc --> HC
HC --> I1
I1 --> I2
I2 --> I3
I3 --> I4
I4 --> API
API -.->|Respuesta| I4
I4 -.-> I3
I3 -.-> I2
I2 -.-> I1
I1 -.-> HC
HC -.-> Svc
Puntos clave¶
- Un interceptor funcional es una función pura que cumple el tipo
HttpInterceptorFn; no requiere clases, decoradores ni implementar interfaces. HttpRequestes inmutable: siempre usamosclone()para crear versiones modificadas antes de pasarlas al siguiente interceptor.- Los interceptores se registran en
provideHttpClient(withInterceptors([...]))dentro deapp.config.ts; el orden del array define el orden de ejecución en la petición. - El flujo es bidireccional: los interceptores actúan como middleware tanto en la petición saliente como en la respuesta entrante.
inject()funciona dentro del cuerpo de la función interceptora, lo que nos da acceso completo al árbol de inyección de dependencias sin boilerplate adicional.
¿Qué sigue?¶
En la parte 2 construiremos el interceptor de autenticación que adjunta automáticamente el JWT a cada petición, gestiona las URLs públicas y maneja el escenario de token expirado.