Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 038 — Middleware, decorador y aspecto#

⬅️ 037 · 📚 Parte 2 · 🎓 Clases · 039 ➡️ Parte 2 — La tubería · Nivel 🔴 avanzado · Pista backendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Distinguir tres alturas para envolver comportamiento, y saber cuál corresponde a cada necesidad. Es la clase que cierra la parte 2 y ordena todo lo anterior.

🧩 La situación#

La misma auditoría implementada en dos alturas distintas. La traza resultante es idéntica en los cuatro frameworks:

externa:GET /accion → interna:accion → manejador → interna:fin

Los marcadores se llaman externa e interna a propósito: cada framework llama distinto a lo mismo, y unificar los nombres es lo que hace comparable el contrato.

📖 Las tres alturas#

Altura Qué ve Qué no ve
Capa de transporte método, ruta, cabeceras, cuerpo crudo qué código va a ejecutarse
Intercepción de manejador qué clase y método se ejecutan, sus argumentos, su resultado nada del transporte que no le pasen
Aspecto sobre el método la ejecución de un método, venga de donde venga absolutamente nada de HTTP

Y cómo lo llama cada framework:

Framework Externa Interna
Spring Boot filtro aspecto
NestJS middleware interceptor
ASP.NET Core middleware filtro de punto final
FastAPI middleware decorador

🧮 El contrato#

Petición Respuesta
GET /accion 200 · {"ok":true}
GET /auditoria ["externa:GET /accion","interna:accion","manejador","interna:fin"]

El segundo caso comprueba las dos cosas a la vez: que las capas envuelven al manejador de fuera adentro, y que se deshacen al revés — la capa interna registra su fin después del manejador y antes de que salga la respuesta.

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Aspecto (Programación orientada a aspectos, AOP) Comportamiento transversal enganchado a la ejecución de un método, no al transporte. A diferencia de un middleware, no sabe nada de HTTP: el mismo aspecto sirve para una petición web, una tarea programada o una prueba.
Decorador Una función que recibe otra función, la envuelve y devuelve la envoltura. En Python y TypeScript tiene sintaxis propia; en JavaScript es una llamada corriente. Registrar una ruta con un decorador y con una llamada a método es el mismo mecanismo.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
NestJS framework de aplicación de Node.js/TypeScript (TypeScript) 2017 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
ASP.NET Core framework web de .NET (C#) 2016 MIT Microsoft y .NET Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente

🔧 NestJS#

Trae a Node.js el modelo de Angular y Spring: módulos, decoradores e inyección de dependencias por constructor.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec tsc -p tsconfig.json

Arrancarla suelta, sin el verificador:

PORT=3000 node dist/main.js

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
src/main.ts código TypeScript
tsconfig.json configuración del compilador de TypeScript

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-038-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java

🔧 ASP.NET Core#

Reescritura multiplataforma y de código abierto de la pila web de Microsoft. Sus API mínimas trajeron el estilo de los microframeworks al ecosistema .NET.

Preparar sus dependencias, dentro de su directorio:

dotnet build -c Release --nologo -v quiet

Arrancarla suelta, sin el verificador:

PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000

Qué hay dentro de su directorio:

Archivo Qué es
Clase038.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Las cuatro producen la misma traza con mecanismos de nombres distintos. Léelas comparando qué información tiene cada altura: la externa solo conoce método y ruta; la interna conoce el método que se va a ejecutar y su resultado.

FastAPI · fastapi/main.py — capa y decorador#

@app.middleware("http")
async def capa(peticion: Request, siguiente):
    if peticion.url.path != "/auditoria":
        auditoria.append(f"externa:{peticion.method} {peticion.url.path}")
    return await siguiente(peticion)
def auditar(funcion: Callable) -> Callable:
    @functools.wraps(funcion)
    def envoltura(*args, **kwargs):
        auditoria.append(f"interna:{funcion.__name__}")
        resultado = funcion(*args, **kwargs)
        auditoria.append("interna:fin")
        return resultado

    return envoltura
@app.get("/accion")
@auditar
def accion() -> JSONResponse:

La diferencia de altura se ve en una línea: la capa escribe peticion.url.path, el decorador escribe funcion.__name__. Una conoce la URL, la otra conoce la función.

Y functools.wraps no es decoración: sin él, la función envuelta pierde su nombre y su firma, y FastAPI deja de poder inspeccionarla para construir la documentación y la validación. Es el fallo clásico de decorar en este ecosistema.

Python no necesita un mecanismo de aspectos aparte porque el decorador ya es la forma nativa de envolver comportamiento.

NestJS · nestjs/src/main.ts — middleware e interceptor#

class CapaExterna implements NestMiddleware {
  use(peticion: Request, respuesta: Response, siguiente: NextFunction): void {
    auditoria.push(`externa:${peticion.method} ${peticion.originalUrl}`);
    siguiente();
  }
}
class CapaInterna implements NestInterceptor {
  intercept(contexto: ExecutionContext, siguiente: CallHandler): Observable<unknown> {
    auditoria.push(`interna:${contexto.getHandler().name}`);
    return siguiente.handle().pipe(tap(() => auditoria.push("interna:fin")));
  }
}

contexto.getHandler().name frente a peticion.method: la misma diferencia de altura, con nombres propios de NestJS. Y el interceptor devuelve un observable, así que puede transformar el resultado además de observarlo.

Un tropiezo real que el código documenta y merece la pena:

    auditoria.push(`externa:${peticion.method} ${peticion.originalUrl}`);

originalUrl y no path. Al montar la capa sobre una ruta concreta con forRoutes("accion"), Express recorta el prefijo de montaje y path vale /. Es el mismo comportamiento que hace que un enrutador anidado vea rutas relativas: cómodo al componer, sorprendente al registrar.

Spring Boot · spring-boot/…/Aplicacion.java — filtro y aspecto#

    public static class CapaFiltro implements Filter {
        @Override
        public void doFilter(ServletRequest peticion, ServletResponse respuesta, FilterChain cadena)
                throws IOException, ServletException {
            HttpServletRequest p = (HttpServletRequest) peticion;
            if (!"/auditoria".equals(p.getRequestURI())) {
                AUDITORIA.add("externa:" + p.getMethod() + " " + p.getRequestURI());
            }
    public static class CapaAspecto {
        @Around("execution(* labs.Aplicacion.Controlador.accion(..))")
        public Object auditar(ProceedingJoinPoint punto) throws Throwable {
            AUDITORIA.add("interna:" + punto.getSignature().getName());
            Object resultado = punto.proceed();
            AUDITORIA.add("interna:fin");
            return resultado;
        }
    }

El aspecto es la pieza más distinta de las cuatro, y la más interesante: no sabe nada de HTTP. Se engancha a la ejecución de un método, sea cual sea quien lo llame.

Por eso el mismo aspecto sirve para una petición web, una tarea programada o una prueba unitaria. Middleware y filtros viven en el transporte; el aspecto vive en el código.

El precio está en la propia línea: execution(* labs.Aplicacion.Controlador.accion(..)) es una expresión de corte, escrita en un lenguaje aparte, que se evalúa en tiempo de ejecución. Renombrar el método rompe el aspecto sin que el compilador diga nada.

ASP.NET Core · aspnet-core/Program.cs — middleware y filtro de punto final#

app.Use(async (contexto, siguiente) =>
{
    if (contexto.Request.Path != "/auditoria")
    {
        auditoria.Add($"externa:{contexto.Request.Method} {contexto.Request.Path}");
    }
    await siguiente();
});
}).AddEndpointFilter(async (contexto, siguiente) =>
{
    auditoria.Add("interna:accion");
    var resultado = await siguiente(contexto);
    auditoria.Add("interna:fin");
    return resultado;
});

Filtro de punto final: ya sabe qué punto final se va a ejecutar, y puede actuar antes y después. Y a diferencia del aspecto de Spring, se registra en la ruta, encadenado al MapGet — así que renombrar el manejador no lo rompe, porque no lo nombra.

Es el punto medio del elenco: la información de la altura interna, con el acoplamiento de la externa.

🎯 La distinción que de verdad importa#

Fíjate en la fila de Spring: su capa interna es un aspecto, y eso lo pone en una categoría aparte.

@Around("execution(* labs.Aplicacion.Controlador.accion(..))")
public Object auditar(ProceedingJoinPoint punto) throws Throwable { ... }

Ese aspecto no sabe nada de HTTP. Se engancha a la ejecución de un método, así que el mismo código funciona si el método lo llama una petición web, una tarea programada, un consumidor de cola o una prueba.

Es la diferencia de fondo de esta clase:

Middleware e interceptores viven en el transporte. El aspecto vive en el código.

La consecuencia práctica: una auditoría implementada como middleware solo audita lo que llega por HTTP. La misma operación disparada por una tarea nocturna pasa sin registrar. Con un aspecto, se audita igual.

🔍 Un detalle que costó una corrección#

La implementación de NestJS registraba externa:GET / en lugar de externa:GET /accion.

Causa: la capa se monta sobre una ruta concreta —forRoutes("accion")— y Express recorta el prefijo de montaje, así que peticion.path vale /. La solución es originalUrl.

Es el mismo comportamiento que hace que un enrutador anidado vea rutas relativas: cómodo al componer, sorprendente al registrar. Y explica por qué muchos registros de peticiones muestran rutas incompletas sin que nadie sepa por qué.

🧭 Cuál elegir#

Necesitas… Altura Por qué
Cabeceras de seguridad, CORS, compresión transporte Son propiedades de la respuesta HTTP
Autenticación, cupos, plazos transporte Deben cortar antes de gastar
Transformar el resultado de un manejador intercepción Ya conoce el resultado
Auditar una operación de negocio aspecto Debe capturarla venga de donde venga
Reintentar o cachear una llamada interna aspecto No tiene nada que ver con HTTP

La regla resumida: cuanto más cerca del dominio esté lo que quieres envolver, más adentro debe estar la capa.

⚠️ Lo que hay que vigilar en los aspectos#

Los aspectos tienen fama de mágicos, y con razón: el código que se ejecuta no está donde se lee.

Un método con un aspecto alrededor se comporta distinto de lo que dice su cuerpo, y nada en el punto de llamada lo indica. Depurar eso exige saber que el aspecto existe.

Además, en Spring los aspectos funcionan mediante proxy, lo que trae una trampa clásica: una llamada desde dentro del mismo objeto no pasa por el proxy, así que el aspecto no se aplica. Es el mismo mecanismo que hace que @Transactional o @Cacheable no funcionen en llamadas internas — uno de los desconciertos más frecuentes con Spring.

🔬 Comparación#

Framework ¿Tiene aspectos reales? Alternativa
Spring Boot , con AspectJ
FastAPI no hace falta: el decorador es nativo decoradores de Python
NestJS no: interceptores, atados a la petición interceptor
ASP.NET Core no de serie filtros, o generación de código

Python es el caso interesante: no necesita un mecanismo de aspectos porque el decorador ya envuelve cualquier función, con o sin HTTP de por medio. Lo que en Java requirió una tecnología aparte, en Python es sintaxis del lenguaje.

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 038

🧪 Reto de transferencia#

Llama al método auditado desde otro método del mismo objeto en la implementación de Spring y comprueba que el aspecto no se aplica. Después explica por qué, y cuál de las dos soluciones habituales —extraer a otro componente o autoinyectarse— prefieres.

🔗 Enlaces#

Fuentes#