Saltearse al contenido

Interceptores

Los interceptores envuelven los handlers de ruta en un pipeline estilo cebolla — antes y después del handler — sin tocar la lógica de negocio. Úsalos para logging, caché, transformación de respuestas, temporización y más.

import type { Interceptor, ExecutionContext } from 'veloce-ts';
class LoggingInterceptor implements Interceptor {
async intercept(
ctx: ExecutionContext,
next: () => Promise<Response>
): Promise<Response> {
const start = Date.now();
const response = await next();
console.log(`${ctx.request.method} ${ctx.request.url}${Date.now() - start}ms`);
return response;
}
}

La llamada a next() ejecuta los interceptores restantes y después el handler de ruta. Todo antes de next() es lógica “pre”; todo después es lógica “post”.

type ExecutionContext = {
request: Request; // Request estándar Web
controller: unknown; // instancia del controlador (puede ser undefined en rutas funcionales)
handler: string; // nombre del método en el controlador
};
const app = new Veloce();
app.useInterceptor(new LoggingInterceptor());
app.useInterceptor(new MetricsInterceptor());
await app.compile();

Los interceptores globales se ejecutan en todas las rutas, antes de los interceptores de clase/método.

import { UseInterceptor } from 'veloce-ts';
@Controller('/users')
@UseInterceptor(LoggingInterceptor) // se aplica a todos los métodos de la clase
class UserController {
@Get('/')
@UseInterceptor(CacheInterceptor) // se apila sobre el nivel de clase
async getUsers() {
return users;
}
}
Interceptores globales (orden de registro)
→ Interceptores de clase (orden del decorador)
→ Interceptores de método (orden del decorador)
→ Handler de ruta
← Interceptores de método (inverso)
← Interceptores de clase (inverso)
← Interceptores globales (inverso)
class TimingInterceptor implements Interceptor {
async intercept(ctx: ExecutionContext, next: () => Promise<Response>) {
const start = performance.now();
const response = await next();
const ms = (performance.now() - start).toFixed(2);
const modified = new Response(response.body, response);
modified.headers.set('X-Response-Time', `${ms}ms`);
return modified;
}
}
const cache = new Map<string, Response>();
class CacheInterceptor implements Interceptor {
async intercept(ctx: ExecutionContext, next: () => Promise<Response>) {
const key = ctx.request.url;
if (cache.has(key)) {
return cache.get(key)!.clone();
}
const response = await next();
if (response.ok) {
cache.set(key, response.clone());
}
return response;
}
}
class AuditInterceptor implements Interceptor {
async intercept(ctx: ExecutionContext, next: () => Promise<Response>) {
const response = await next();
if (response.status === 401 || response.status === 403) {
console.warn(`Intento de acceso no autorizado: ${ctx.request.url}`);
}
return response;
}
}
class WrapResponseInterceptor implements Interceptor {
async intercept(ctx: ExecutionContext, next: () => Promise<Response>) {
const response = await next();
if (!response.headers.get('content-type')?.includes('json')) {
return response;
}
const data = await response.json();
return Response.json({ data, timestamp: Date.now() }, response);
}
}
  • Mantén los interceptores sin estado — inyecta el estado vía constructor o clausura.
  • Siempre llama a next() a menos que quieras cortocircuitar el pipeline.
  • Devuelve una Response clonada al añadir cabeceras — Response es inmutable.
  • Registra los interceptores de uso frecuente globalmente; los específicos de funcionalidad con @UseInterceptor.