Vamos con nuevo post de la serie NestJS, este es el número 14 y segundo del bloque de Funcionalidades Avanzadas. En el post anterior montamos Exception Filters para gestionar errores de forma centralizada y hoy nos metemos con la pieza más versátil del pipeline que son los Interceptors.
Los Interceptors son la navaja suiza de NestJS. Con el mismo patrón puedes hacer logging, transformar respuestas, cachear datos, medir tiempos, controlar timeouts y más. Son la implementación de NestJS del patrón AOP AOP Aspect-Oriented Programming. Patrón que permite añadir comportamiento transversal (logging, caching, transformación) sin modificar la lógica de negocio. Los Interceptors son la implementación de AOP en NestJS. .
Y también así de gratis vamos a ver decoradores custom para hacer que el código sea más expresivo y limpio.
Toda la base: Docker (post 1), controllers (post 2), DI (post 3), módulos (post 4), middleware y pipeline (post 5), validación (post 6), PostgreSQL con TypeORM (posts 7-9), seguridad (posts 10-12) y Exception Filters (post 13).
EA, al lío que viene el tío.

1. ¿Qué es un Interceptor?
Un 💡 Interceptor Clase que implementa NestInterceptor y puede ejecutar lógica ANTES y DESPUÉS del handler. Tiene acceso al ExecutionContext y al Observable de la respuesta. Se usa para logging, transformación de datos, caching, timeout, wrapping de respuestas y más. Más info → es una clase que envuelve el handler como una cebolla porque ejecuta código antes de que la request llegue al handler y después de que el handler devuelva la respuesta.
Request → Middleware → Guards → Interceptor (ANTES) → Pipes → Handler
↓
Response ← Exception Filter ← Interceptor (DESPUÉS) ←←←←←←←←←←←←
Vamos a ver la diferencia que hay con el middleware y los Guards:
| Pieza | Cuándo ejecuta | Acceso a | Uso principal |
|---|---|---|---|
| Middleware | Solo ANTES del handler | Request/Response raw | Modificar request, logging básico |
| Guard | Solo ANTES del handler | ExecutionContext + metadata | Autenticación, autorización |
| Interceptor | ANTES y DESPUÉS del handler | ExecutionContext + Observable de la respuesta | Logging, transformación, cache, timeout |
Los Interceptors son la única pieza del pipeline que puede modificar la respuesta que sale del handler.
2. Anatomía de un Interceptor
Todos los Interceptors implementan la interfaz NestInterceptor:
import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
import { type Observable } from 'rxjs';
export interface NestInterceptor<T = unknown, R = unknown> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<R> | Promise<Observable<R>>;
}
Dos parámetros tengo:
context: ExecutionContext— El mismo que en los Guards (post 11). Acceso al controller, handler, metadata, request, response.next: CallHandler— Representa el handler al que va a llegar la request. Tiene un métodohandle()que devuelve unObservablecon la respuesta del handler.
En los Interceptors es 💡 CallHandler Interfaz que representa la continuación del pipeline. Su método handle() ejecuta el handler del controller y devuelve un Observable con la respuesta. Si no llamas a next.handle(), el handler NO se ejecuta — el interceptor corta el pipeline.
Más info →
quién parte el bacalao y si no llamas a next.handle(), el handler nunca se ejecuta, y esto te permite:
- Ejecutar lógica antes del handler (antes de
next.handle()). - Ejecutar lógica después del handler (operadores RxJS sobre el Observable).
- No ejecutar el handler (devolviendo un Observable sin llamar a
next.handle()).
3. RxJS: lo justo y necesario
Los Interceptors usan 💡 RxJS Reactive Extensions for JavaScript. Librería para programación reactiva con Observables. NestJS la usa internamente para el pipeline de requests. En los Interceptors, usas operadores RxJS (tap, map, catchError, timeout) para manipular el flujo de la respuesta. Más info → internamente. No hace falta ser un pro en RxJS, pero sí entender estos operadores para cubrir todo lo que necesitarás en Interceptors:
| Operador | Qué hace | Modifica la respuesta |
|---|---|---|
tap() | Ejecuta un side effect sin modificar el valor | No |
map() | Transforma el valor emitido | Sí |
catchError() | Captura errores y puede devolver un valor alternativo | Sí (en caso de error) |
timeout() | Lanza error si el handler no responde a tiempo | No (lanza excepción) |
of() | Crea un Observable con un valor fijo | Sí (reemplaza la respuesta) |
4. Logging Interceptor
El caso de uso más clásico de Interceptor es Loggear cada request con método, URL, tiempo de respuesta y status code:
// src/common/interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { type Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { type Request, type Response } from 'express';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<Request>();
const { method, url } = request;
const startTime = Date.now();
// ANTES del handler: loggear la entrada
this.logger.log(`→ ${method} ${url}`);
return next.handle().pipe(
// DESPUÉS del handler: loggear la salida con el tiempo
tap({
next: () => {
const response = context.switchToHttp().getResponse<Response>();
const elapsed = Date.now() - startTime;
this.logger.log(
`← ${method} ${url} ${response.statusCode} [${elapsed}ms]`,
);
},
error: (error: Error) => {
const elapsed = Date.now() - startTime;
this.logger.error(
`← ${method} ${url} ERROR [${elapsed}ms]: ${error.message}`,
);
},
}),
);
}
}
La salida en consola sería:
[LoggingInterceptor] → GET /users
[LoggingInterceptor] ← GET /users 200 [12ms]
[LoggingInterceptor] → POST /auth/login
[LoggingInterceptor] ← POST /auth/login 201 [145ms]
[LoggingInterceptor] → GET /users/uuid-inexistente
[LoggingInterceptor] ← GET /users/uuid-inexistente ERROR [3ms]: Usuario no encontrado
Mira cómo funciona tap(): recibe next (éxito) y error (fallo). En los dos casos calcula el tiempo transcurrido y no modifica la respuesta, solo la observa.
5. Transform Response Interceptor
Casi todas las APIs de producción envuelven las respuestas en un formato estándar. En vez de devolver directamente el array de usuarios, devuelves:
{
"data": [...],
"statusCode": 200,
"timestamp": "2026-03-21T10:30:00.000Z"
}
Un Interceptor que hace esto automáticamente para todos los endpoints:
// src/common/interceptors/transform-response.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { type Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { type Response } from 'express';
export interface ApiResponse<T> {
data: T;
statusCode: number;
timestamp: string;
}
@Injectable()
export class TransformResponseInterceptor<T>
implements NestInterceptor<T, ApiResponse<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<ApiResponse<T>> {
const response = context.switchToHttp().getResponse<Response>();
return next.handle().pipe(
map((data) => ({
data,
statusCode: response.statusCode,
timestamp: new Date().toISOString(),
})),
);
}
}
Antes del interceptor:
[
{ "id": "uuid-1", "name": "Domin", "email": "domin@domin.es" },
{ "id": "uuid-2", "name": "Otro", "email": "otro@domin.es" }
]
Después del interceptor:
{
"data": [
{ "id": "uuid-1", "name": "Domin", "email": "domin@domin.es" },
{ "id": "uuid-2", "name": "Otro", "email": "otro@domin.es" }
],
"statusCode": 200,
"timestamp": "2026-03-21T10:30:00.000Z"
}
TransformResponseInterceptor<T> es un Interceptor genérico. No importa si el handler devuelve un User, un User[] o un PaginatedResult<User> porque todo queda envuelto en { data, statusCode, timestamp }.
Cuidado porque si ya tienes endpoints que devuelven
PaginatedResult<User>con{ data, meta }, este interceptor los envuelve otra vez:{ data: { data: [...], meta: {...} } }. Puedes usar un decorador para excluir endpoints específicos (lo vemos en la sección 12).
6. Timeout Interceptor
Un handler que tarda demasiado bloquea un hilo de Node.js, así que lo suyo es meterle un límite:
// src/common/interceptors/timeout.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
RequestTimeoutException,
} from '@nestjs/common';
import { type Observable, throwError, TimeoutError } from 'rxjs';
import { timeout, catchError } from 'rxjs/operators';
const DEFAULT_TIMEOUT_MS = 15000;
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(private readonly timeoutMs: number = DEFAULT_TIMEOUT_MS) {}
intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(
timeout(this.timeoutMs),
catchError((error) => {
if (error instanceof TimeoutError) {
return throwError(
() =>
new RequestTimeoutException(
`La operación excedió el tiempo límite de ${this.timeoutMs}ms`,
),
);
}
return throwError(() => error);
}),
);
}
}
Con esto, si el handler tarda más de 15 segundos, el interceptor lanza RequestTimeoutException (HTTP 408).
El catchError solo captura TimeoutError y cualquier otro error se deja pasar para que lo gestione el Exception Filter del post 13.
7. Cache Interceptor simple
Un interceptor que cachea respuestas en memoria para endpoints que no cambian frecuentemente:
// src/common/interceptors/cache.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { type Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { type Request } from 'express';
@Injectable()
export class SimpleCacheInterceptor implements NestInterceptor {
private readonly cache = new Map<string, { data: unknown; expiry: number }>();
private readonly ttlMs: number;
constructor(ttlMs: number = 30000) {
this.ttlMs = ttlMs;
}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
// Solo cachear GET requests
const request = context.switchToHttp().getRequest<Request>();
if (request.method !== 'GET') {
return next.handle();
}
const cacheKey = request.url;
const cached = this.cache.get(cacheKey);
// Cache hit: devolver sin ejecutar el handler
if (cached && cached.expiry > Date.now()) {
return of(cached.data);
}
// Cache miss: ejecutar handler y guardar resultado
return next.handle().pipe(
tap((data) => {
this.cache.set(cacheKey, {
data,
expiry: Date.now() + this.ttlMs,
});
}),
);
}
}
En este caso debemos fijarnos en of(cached.data) porque crea un Observable con el valor cacheado sin llamar a next.handle(). El handler no se ejecuta y la respuesta viene del cache directamente.
Para producción mejor usa
@nestjs/cache-managercon Redis en vez de unMapen memoria. Lo veremos en el post 26. Este ejemplo es para entender el patrón.
8. Exclude Interceptor Decorator
Algunos endpoints no deberían tener ciertos interceptors (como la transformación de respuesta o el cache). Crea un decorador para excluirlos:
// src/common/decorators/skip-transform.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const SKIP_TRANSFORM_KEY = 'skipTransform';
export const SkipTransform = () => SetMetadata(SKIP_TRANSFORM_KEY, true);
Y modifica el TransformResponseInterceptor para respetarlo:
// src/common/interceptors/transform-response.interceptor.ts
import { Reflector } from '@nestjs/core';
import { SKIP_TRANSFORM_KEY } from '../decorators/skip-transform.decorator';
@Injectable()
export class TransformResponseInterceptor<T>
implements NestInterceptor<T, ApiResponse<T> | T>
{
constructor(private readonly reflector: Reflector) {}
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<ApiResponse<T> | T> {
const skipTransform = this.reflector.getAllAndOverride<boolean>(
SKIP_TRANSFORM_KEY,
[context.getHandler(), context.getClass()],
);
if (skipTransform) {
return next.handle();
}
const response = context.switchToHttp().getResponse<Response>();
return next.handle().pipe(
map((data) => ({
data,
statusCode: response.statusCode,
timestamp: new Date().toISOString(),
})),
);
}
}
Así se usa:
// La respuesta se transforma normalmente
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
// La respuesta sale tal cual del handler
@Get('export')
@SkipTransform()
exportCsv(): string {
return 'id,name,email\n1,Domin,domin@domin.es';
}
El mismo patrón que @Public() del post 10 y @SkipThrottle() del post 12. Metadata + Reflector en el interceptor.
9. Niveles de aplicación
Como los Guards y los Pipes, los Interceptors se aplican a tres niveles:
9.1. A nivel de método
@Get(':id')
@UseInterceptors(LoggingInterceptor)
findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
return this.usersService.findOne(id);
}
9.2. A nivel de controller
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
// Todos los endpoints del controller pasan por el interceptor
}
9.3. A nivel global (recomendado para logging y transform)
// src/app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
import { TransformResponseInterceptor } from './common/interceptors/transform-response.interceptor';
import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: TransformResponseInterceptor,
},
{
provide: APP_INTERCEPTOR,
useFactory: () => new TimeoutInterceptor(15000),
},
],
})
export class AppModule {}
Con APP_INTERCEPTOR los interceptors participan en el contenedor de DI. Puedes inyectar ConfigService, Reflector, repositorios, lo que necesites.
Orden de ejecución: los interceptors globales se ejecutan en el orden en que se registran. Primero
LoggingInterceptor(loguea la entrada), luegoTransformResponseInterceptor(transforma la salida), luegoTimeoutInterceptor(controla el tiempo). El before va en orden de registro, el after va en orden inverso (como una pila).
10. Custom Param Decorators: createParamDecorator
Ya vimos @GetUser() en el post 10. Ahora vamos a profundizar en createParamDecorator y crear más decoradores útiles.
10.1. @GetUser() (recordatorio)
// src/auth/decorators/get-user.decorator.ts
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import { type JwtPayload } from '../interfaces/jwt-payload.interface';
export const GetUser = createParamDecorator(
(data: keyof JwtPayload | undefined, ctx: ExecutionContext): JwtPayload | string => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as JwtPayload;
return data ? user[data] : user;
},
);
El primer parámetro (data) es lo que pasas al decorador: @GetUser('sub') → data = 'sub'. El segundo (ctx) es el ExecutionContext.
10.2. @ClientIp(): IP real del cliente
Detrás de un proxy, req.ip devuelve la IP del proxy, no del cliente. Necesitas leer el header X-Forwarded-For:
// src/common/decorators/client-ip.decorator.ts
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import { type Request } from 'express';
export const ClientIp = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request>();
const forwarded = request.headers['x-forwarded-for'];
if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim();
}
return request.ip ?? '0.0.0.0';
},
);
@Post('login')
login(@ClientIp() ip: string, @Body() loginDto: LoginDto): Promise<AuthTokens> {
this.logger.log(`Login attempt from ${ip}`);
return this.authService.login(loginDto);
}
10.3. @RequestHeader(): header tipado
// src/common/decorators/request-header.decorator.ts
import {
createParamDecorator,
type ExecutionContext,
BadRequestException,
} from '@nestjs/common';
import { type Request } from 'express';
export const RequestHeader = createParamDecorator(
(headerName: string, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request>();
const value = request.headers[headerName.toLowerCase()];
if (!value || typeof value !== 'string') {
throw new BadRequestException(
`Header "${headerName}" es obligatorio`,
);
}
return value;
},
);
@Post('webhook')
handleWebhook(
@RequestHeader('X-Webhook-Signature') signature: string,
@Body() payload: unknown,
): Promise<void> {
// signature está garantizado como string, no undefined
return this.webhookService.process(payload, signature);
}
10.4. @Pagination(): extraer paginación del query
// src/common/decorators/pagination.decorator.ts
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import { type Request } from 'express';
export interface PaginationParams {
page: number;
limit: number;
skip: number;
}
export const Pagination = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): PaginationParams => {
const request = ctx.switchToHttp().getRequest<Request>();
const page = Math.max(1, parseInt(request.query.page as string, 10) || 1);
const limit = Math.min(100, Math.max(1, parseInt(request.query.limit as string, 10) || 10));
const skip = (page - 1) * limit;
return { page, limit, skip };
},
);
@Get()
findAll(@Pagination() pagination: PaginationParams): Promise<PaginatedResult<User>> {
return this.usersService.findAll(pagination);
}
El decorador parsea, valida (mínimo 1, máximo 100) y calcula el skip y así el controller se queda sin nada, limpio.
11. applyDecorators: composición de decoradores
Cuando repites la misma combinación de decoradores en varios endpoints, applyDecorators los fusiona en uno:
// src/common/decorators/api-paginated.decorator.ts
import { applyDecorators, Get, UseInterceptors } from '@nestjs/common';
import { TransformResponseInterceptor } from '../interceptors/transform-response.interceptor';
import { LoggingInterceptor } from '../interceptors/logging.interceptor';
export const ApiPaginatedEndpoint = (path?: string) =>
applyDecorators(
Get(path),
UseInterceptors(LoggingInterceptor),
);
// src/common/decorators/admin-endpoint.decorator.ts
import { applyDecorators, UseGuards } from '@nestjs/common';
import { Roles } from '../../auth/decorators/roles.decorator';
import { UserRole } from '../../users/enums/user-role.enum';
export const AdminEndpoint = () =>
applyDecorators(
Roles(UserRole.ADMIN),
);
Uso:
@Controller('users')
export class UsersController {
@ApiPaginatedEndpoint()
findAll(@Pagination() pagination: PaginationParams): Promise<PaginatedResult<User>> {
return this.usersService.findAll(pagination);
}
@Delete(':id')
@AdminEndpoint()
remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
return this.usersService.remove(id);
}
}
applyDecorators acepta cualquier decorador de NestJS y combina decoradores de método (@Get, @Post), Guards (@UseGuards), Interceptors (@UseInterceptors), metadata (@SetMetadata) y los tuyos propios.
12. Interceptor con inyección de dependencias
Un Interceptor que lee configuración del ConfigService:
// src/common/interceptors/performance.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { type Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { type Request } from 'express';
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
private readonly logger = new Logger(PerformanceInterceptor.name);
private readonly slowThresholdMs: number;
constructor(configService: ConfigService) {
this.slowThresholdMs = configService.get<number>(
'SLOW_REQUEST_THRESHOLD_MS',
3000,
);
}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<Request>();
const startTime = Date.now();
return next.handle().pipe(
tap(() => {
const elapsed = Date.now() - startTime;
if (elapsed > this.slowThresholdMs) {
this.logger.warn(
`🐢 Slow request: ${request.method} ${request.url} [${elapsed}ms] > ${this.slowThresholdMs}ms threshold`,
);
}
}),
);
}
}
Solo loggea requests lentas, no todas. Si una petición tarda más del umbral configurado, la marca como warning. Esta implementación es genial para detectar problemas de rendimiento sin petar los logs.
Regístralo con APP_INTERCEPTOR para que tenga acceso a ConfigService:
{
provide: APP_INTERCEPTOR,
useClass: PerformanceInterceptor,
},
13. Orden completo del pipeline (actualizado)
Después de 14 posts, el pipeline completo de NestJS queda así, de momento:
Request
↓
1. Middleware (Post 5) — Modificar request, logging básico
↓
2. Guards (Post 11) — Autenticación, autorización
↓
3. Interceptors ANTES (Post 14) — Logging, timing, cache check
↓
4. Pipes (Post 6) — Validación, transformación
↓
5. Handler (Post 2) — Tu lógica de negocio
↓
6. Interceptors DESPUÉS (Post 14) — Transform response, cache save
↓
7. Exception Filters (Post 13) — Captura errores de cualquier capa
↓
Response
Con los Guards globales del post 12:
Request → Throttler → JwtAuth → Roles → Interceptors → Pipes → Handler → Interceptors → Response
↓
Exception Filters
Cada capa tiene su responsabilidad y ninguna se solapa con otra.
14. Errores comunes
Error 1: No llamar a next.handle()
// ❌ El handler NUNCA se ejecuta, el endpoint no devuelve nada
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
console.log('antes');
// Olvidaste return next.handle()
return of(undefined);
}
// ✅ Siempre llama a next.handle() a menos que intencionalmente cortes el flujo
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
console.log('antes');
return next.handle();
}
Error 2: Usar tap() cuando necesitas map()
// ❌ tap() no modifica la respuesta, la data original se envía al cliente
return next.handle().pipe(
tap((data) => ({ wrapped: data })), // El return se ignora
);
// ✅ map() sí modifica la respuesta
return next.handle().pipe(
map((data) => ({ wrapped: data })),
);
tap() es para side effects (logging, métricas). map() es para transformar el valor.
Error 3: Interceptor global sin APP_INTERCEPTOR
// ❌ Sin DI, no puedes inyectar nada en el interceptor
app.useGlobalInterceptors(new PerformanceInterceptor(???));
// PerformanceInterceptor necesita ConfigService pero no tiene acceso
// ✅ Con APP_INTERCEPTOR, el contenedor de DI inyecta las dependencias
{
provide: APP_INTERCEPTOR,
useClass: PerformanceInterceptor,
}
Error 4: Duplicar el wrapper de respuesta
// ❌ TransformResponseInterceptor envuelve { data: ... }
// y el handler ya devuelve { data: users, meta: pagination }
// Resultado: { data: { data: users, meta: pagination }, statusCode: 200 }
// ✅ Usa @SkipTransform() en endpoints que ya tienen su propio formato
@Get()
@SkipTransform()
findAll(): Promise<PaginatedResult<User>> {
return this.usersService.findAll();
}
15. Estructura de ficheros actualizada
src/
├── common/
│ ├── decorators/
│ │ ├── client-ip.decorator.ts
│ │ ├── pagination.decorator.ts
│ │ ├── request-header.decorator.ts
│ │ ├── skip-transform.decorator.ts
│ │ ├── api-paginated.decorator.ts
│ │ └── admin-endpoint.decorator.ts
│ ├── exceptions/ ← Post 13
│ │ ├── entity-not-found.exception.ts
│ │ ├── duplicate-entity.exception.ts
│ │ └── business-rule.exception.ts
│ ├── filters/ ← Post 13
│ │ ├── all-exceptions.filter.ts
│ │ ├── http-exception.filter.ts
│ │ └── typeorm-exception.filter.ts
│ ├── guards/ ← Post 12
│ │ └── custom-throttler.guard.ts
│ ├── interceptors/
│ │ ├── logging.interceptor.ts
│ │ ├── transform-response.interceptor.ts
│ │ ├── timeout.interceptor.ts
│ │ ├── cache.interceptor.ts
│ │ └── performance.interceptor.ts
│ └── interfaces/
│ ├── api-response.interface.ts
│ ├── error-response.interface.ts
│ └── paginated-result.interface.ts
16. Recapitulando
Interfaz con intercept(context, next). next.handle() ejecuta el handler y devuelve un Observable. Lógica antes y después.
tap() para side effects (logging). map() para transformar respuestas. timeout() para límites. catchError() para errores.
Loguea método, URL, status code y tiempo de respuesta. tap() con next y error.
Envuelve todas las respuestas en { data, statusCode, timestamp }. map() sobre el Observable.
@GetUser(), @ClientIp(), @Pagination(). Extraen datos de la request de forma tipada y reutilizable.
Compone múltiples decoradores en uno. @AdminEndpoint() en vez de repetir @Roles() + @UseGuards() + @UseInterceptors().
En el próximo post nos metemos con la serialización y transformación de datos con ClassSerializerInterceptor, @Exclude, @Expose, @Transform, @Type, groups y plainToInstance. Así vamos a poder saber exactamente qué datos salen de la API y en qué forma.
EA, nos vemos en los bares!! 🍺
Pon a prueba lo aprendido
1. ¿Qué pasa si un Interceptor no llama a next.handle()?
2. ¿Cuál es la diferencia entre tap() y map() en un Interceptor?
3. ¿Cuándo se ejecutan los Interceptors en el pipeline de NestJS?
4. ¿Por qué es mejor registrar Interceptors con APP_INTERCEPTOR que con useGlobalInterceptors()?
5. ¿Para qué sirve createParamDecorator?