🚨 ¡Nueva review! 🔇 Los mejores cascos con ANC del mercado: los Sony WH-1000XM4 . ¡Échale un ojo! 👀

Interceptors y Custom Decorators en NestJS

Serie NestJS #14 — NestInterceptor, CallHandler, Observable, logging, transformación de respuestas, timeout, createParamDecorator y applyDecorators

Escrito por domin el 12 de abril de 2026

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 .

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.

Diagrama de interceptors envolviendo el handler en NestJS como capas de cebolla.

1. ¿Qué es un Interceptor?

Un 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:

PiezaCuándo ejecutaAcceso aUso principal
MiddlewareSolo ANTES del handlerRequest/Response rawModificar request, logging básico
GuardSolo ANTES del handlerExecutionContext + metadataAutenticación, autorización
InterceptorANTES y DESPUÉS del handlerExecutionContext + Observable de la respuestaLogging, 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:

En los Interceptors es quién parte el bacalao y si no llamas a next.handle(), el handler nunca se ejecuta, y esto te permite:

  1. Ejecutar lógica antes del handler (antes de next.handle()).
  2. Ejecutar lógica después del handler (operadores RxJS sobre el Observable).
  3. No ejecutar el handler (devolviendo un Observable sin llamar a next.handle()).

3. RxJS: lo justo y necesario

Los Interceptors usan internamente. No hace falta ser un pro en RxJS, pero sí entender estos operadores para cubrir todo lo que necesitarás en Interceptors:

OperadorQué haceModifica la respuesta
tap()Ejecuta un side effect sin modificar el valorNo
map()Transforma el valor emitido
catchError()Captura errores y puede devolver un valor alternativoSí (en caso de error)
timeout()Lanza error si el handler no responde a tiempoNo (lanza excepción)
of()Crea un Observable con un valor fijoSí (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-manager con Redis en vez de un Map en 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), luego TransformResponseInterceptor (transforma la salida), luego TimeoutInterceptor (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

🧅 NestInterceptor

Interfaz con intercept(context, next). next.handle() ejecuta el handler y devuelve un Observable. Lógica antes y después.

🔧 Operadores RxJS

tap() para side effects (logging). map() para transformar respuestas. timeout() para límites. catchError() para errores.

📝 Logging Interceptor

Loguea método, URL, status code y tiempo de respuesta. tap() con next y error.

📦 Transform Response

Envuelve todas las respuestas en { data, statusCode, timestamp }. map() sobre el Observable.

🏷️ createParamDecorator

@GetUser(), @ClientIp(), @Pagination(). Extraen datos de la request de forma tipada y reutilizable.

🧩 applyDecorators

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?