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

Clase 026 — El patrón middleware#

⬅️ 025 · 📚 Parte 2 · 🎓 Clases · 027 ➡️ Parte 2 — La tubería · Nivel 🟢 introductorio · Pista backendClase construida — 10 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Reconocer la misma idea con cinco nombres distintos, y entender por qué todos los frameworks de servidor acabaron adoptándola.

📖 Un patrón con muchos nombres#

Framework Cómo lo llama Cómo continúa la cadena
Express middleware siguiente()
Fastify gancho (onRequest) por fase, sin llamada explícita
FastAPI middleware await siguiente(peticion)
Flask ganchos (after_request) por fase
Django middleware siguiente(peticion)
Spring Boot filtro / interceptor cadena.doFilter(...)
ASP.NET Core middleware await siguiente()
Laravel middleware $siguiente($peticion)
Rails middleware de Rack @app.call(env)
Gin middleware c.Next()

Debajo de los diez está la cadena de responsabilidad del catálogo de patrones [gof-design-patterns]: una serie de objetos que reciben una petición y deciden si la atienden o la pasan al siguiente.

🧩 La situación#

Una capa intermedia añade x-capa: intermedia a todas las respuestas: a /a, a /b y también al 404 de una ruta que no existe. Ninguna de las rutas sabe que esa capa existe.

🧮 El contrato#

Petición Respuesta
GET /a 200 · {"ruta":"a"} · x-capa: intermedia
GET /b 200 · {"ruta":"b"} · x-capa: intermedia
GET /no-existe 404 · también x-capa: intermedia
GET /tampoco 404 · {"error":"no existe"}

El tercer caso es el importante. Si el 404 lleva la cabecera, la capa se ejecutó antes del enrutado — que es lo que distingue una capa de la tubería de un decorador de ruta. Y es lo que hace que la autenticación, el registro y la correlación puedan cubrir rutas que aún no existen.

<!-- 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
Middleware (Capa, Filtro, Interceptor) Una pieza que envuelve al manejador: recibe la petición, hace su parte y llama —o no— a la siguiente. La cadena se recorre hacia dentro y se deshace hacia fuera. Cada ecosistema le da un nombre distinto y el mecanismo es el mismo.

🧰 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
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
Fastify framework web de Node.js (JavaScript/TypeScript) 2016 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Flask framework web de Python (Python) 2010 BSD-3-Clause Pallets Projects
Django framework web de Python (Python) 2005 BSD-3-Clause Django Software Foundation
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
Laravel full-stack-framework de PHP (PHP) 2011 MIT proyecto independiente
Ruby on Rails full-stack-framework de Ruby (Ruby) 2004 MIT proyecto independiente
Gin framework web de Go (Go) 2014 MIT proyecto independiente

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

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
server.mjs código JavaScript (módulo ES)

🔧 Fastify#

Validación y serialización derivadas de JSON Schema, con un sistema de plugins con encapsulamiento explícito.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

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
server.mjs código JavaScript (módulo ES)

🔧 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

🔧 Flask#

Microframework que dejó a la persona elegir ORM, validación y estructura. El contrapunto exacto de Django dentro del mismo lenguaje.

Arrancarla suelta, sin el verificador:

PORT=3000 python app.py

Qué hay dentro de su directorio:

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

🔧 Django#

Baterías incluidas: ORM, migraciones, panel de administración, autenticación y formularios. Su panel generado sigue siendo un argumento decisivo para productos internos.

Arrancarla suelta, sin el verificador:

PORT=3000 python app.py

Qué hay dentro de su directorio:

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

🔧 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-026-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
src/main/resources/application.properties configuración de Spring Boot: lo que se ajusta sin tocar el código

🔧 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
Clase026.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

🔧 Laravel#

El framework más usado de PHP: ORM Eloquent, migraciones, colas, programación de tareas, pruebas y un ecosistema comercial propio. Redefinió lo que se espera de la experiencia de desarrollo en el lenguaje.

Preparar sus dependencias, dentro de su directorio:

composer install --no-interaction --quiet

Arrancarla suelta, sin el verificador:

PORT=3000 php -S 127.0.0.1:3000 -t public

Qué hay dentro de su directorio:

Archivo Qué es
bootstrap/app.php arranque de Laravel: qué grupo de rutas, qué capas y qué manejo de errores
bootstrap/providers.php código PHP
composer.json manifiesto de Composer: la versión de PHP y las bibliotecas del proyecto
config/app.php código PHP
config/cache.php código PHP
config/session.php código PHP
config/view.php código PHP
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

🔧 Ruby on Rails#

Origen de «convención sobre configuración» y de las migraciones de base de datos tal como se entienden hoy. Casi todos los frameworks completos posteriores citan su influencia.

Preparar sus dependencias, dentro de su directorio:

bundle install --quiet

Arrancarla suelta, sin el verificador:

PORT=3000 bundle exec puma -b tcp://127.0.0.1:3000 config.ru

Qué hay dentro de su directorio:

Archivo Qué es
.bundle/config archivo del proyecto
Gemfile dependencias de Ruby
config.ru punto de entrada de Rack, el estándar de servidores de Ruby
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

🔧 Gin#

El framework HTTP más usado de Go: enrutado rápido y middleware, sobre la biblioteca estándar.

Preparar sus dependencias, dentro de su directorio:

go mod tidy

Arrancarla suelta, sin el verificador:

PORT=3000 go run main.go

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
go.mod módulo de Go: su nombre, la versión del lenguaje y sus dependencias
main.go código Go

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#

Diez frameworks, cuatro formas distintas del mismo patrón: una cadena con llamada explícita, un objeto que envuelve a otro, una fábrica que se ejecuta una vez, y ganchos con nombre por fase.

La forma más desnuda · Rails, con Rack · rails/config.ru#

class Capa
  def initialize(app)
    @app = app
  end

  def call(env)
    estado, cabeceras, cuerpo = @app.call(env)
    cabeceras["x-capa"] = "intermedia"
    [estado, cabeceras, cuerpo]
  end
end
  config.middleware.use Capa

Un objeto que envuelve a otro y responde a call. Eso es todo el patrón.

Rails no inventó nada aquí: hereda Rack entero, y Rack es la especificación mínima del patrón en Ruby. Empieza por este bloque aunque no sepas Ruby — los otros nueve son variaciones sobre estas nueve líneas.

Fíjate en que la respuesta es una tupla que se desarma y se rearma: estado, cabeceras y cuerpo. No hay objeto respuesta con métodos; hay tres valores.

Express · express/server.mjs — la referencia#

app.use((peticion, respuesta, siguiente) => {
  respuesta.set("x-capa", "intermedia");
  siguiente();
});

siguiente() es explícito y obligatorio. Sin esa llamada la cadena se detiene y la petición se queda colgada — es la fuente número uno de peticiones que nunca responden en Express, y no produce ningún error: produce silencio.

app.use((peticion, respuesta) => respuesta.status(404).json({ error: "no existe" }));

Y el 404 es otra capa, la última. En Express todo es la misma cadena: las rutas, las capas y el error final.

FastAPI · fastapi/main.py#

@app.middleware("http")
async def capa(peticion: Request, siguiente):
    respuesta = await siguiente(peticion)
    respuesta.headers["x-capa"] = "intermedia"
    return respuesta

await siguiente(peticion) es el next() de Express con otro nombre — y con una diferencia que ya apareció en la clase 028: devuelve la respuesta en vez de modificar una que ya existe.

Django · django/app.py — una fábrica, no una función#

def capa(siguiente):
    def procesar(peticion):
        respuesta = siguiente(peticion)
        respuesta["X-Capa"] = "intermedia"
        return respuesta

    return procesar
    MIDDLEWARE=[f"{__name__}.capa"],

Dos funciones anidadas, y la razón es concreta: la externa se ejecuta una vez al arrancar y la interna en cada petición.

Ese hueco entre las dos es el sitio correcto para el trabajo caro de inicialización —abrir un fichero de configuración, compilar una expresión regular, montar un cliente HTTP— que no debe repetirse en cada petición. Ninguno de los otros nueve lo ofrece tan claramente.

Y el registro es una cadena de texto con la ruta al objeto, no una referencia: Django lo resuelve al arrancar.

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

    public static class Capa implements Filter {
        @Override
        public void doFilter(ServletRequest peticion, ServletResponse respuesta, FilterChain cadena)
                throws IOException, ServletException {
            ((HttpServletResponse) respuesta).setHeader("X-Capa", "intermedia");
            cadena.doFilter(peticion, respuesta);
        }
    }

En el mundo de los servlets el patrón se llama filtro, y cadena.doFilter es el siguiente() de Express. Existe desde mucho antes que Express — la especificación de Servlet lo trae desde 2000.

Spring tiene además interceptores, que actúan más adentro: después del enrutado y sabiendo qué método va a ejecutarse. La clase 038 compara las dos alturas.

Laravel · laravel/bootstrap/app.php — una clase con handle#

class Capa
{
    public function handle(Request $peticion, Closure $siguiente)
    {
        $respuesta = $siguiente($peticion);
        $respuesta->headers->set('X-Capa', 'intermedia');

        return $respuesta;
    }
}
        $middleware->append(Capa::class);

Laravel no acepta una función anónima aquí, y el motivo es práctico: al registrar la capa por su nombre de clase, el contenedor puede construirla inyectándole dependencias (clase 036) y reutilizarla por nombre en grupos de rutas.

Al montar esta implementación, pasar una función anónima produjo un error de tipo directo. La restricción es deliberada, no un descuido.

Fastify · fastify/server.mjs — ganchos por fase, no una cadena#

app.addHook("onRequest", async (peticion, respuesta) => {
  respuesta.header("x-capa", "intermedia");
});

Fastify no usa la cadena de Express. Usa ganchos con nombre, uno por fase del ciclo: onRequest, preHandler, onSend, onResponse.

No hay siguiente() que olvidar —y por tanto no existe la petición colgada de Express— y a cambio hay que saber en qué fase entra cada cosa. Se cambia un error frecuente por una decisión que hay que aprender.

Es además una decisión con consecuencias medibles: sin cadena que recorrer, el coste por petición es menor.

Flask · flask/app.py — ganchos también#

@app.after_request
def capa(respuesta):
    respuesta.headers["X-Capa"] = "intermedia"
    return respuesta

Flask está en el mismo grupo que Fastify: no tiene middleware propio, tiene ganchos por fase. after_request se ejecuta con la respuesta ya construida — incluida la de un 404, que es lo que este contrato exige.

Por debajo sí hay middleware WSGI, que es el equivalente de Rack en Python. Los ganchos son la capa cómoda por encima.

Gin · gin/main.goNext() en medio#

	motor.Use(func(c *gin.Context) {
		c.Header("X-Capa", "intermedia")
		c.Next()
	})

Lo interesante de Gin es que Next() está en medio: lo que escribas después se ejecuta al volver, con la respuesta ya generada.

Es la forma más visual del elenco de ver que la cadena se recorre hacia dentro y se deshace hacia fuera — la propiedad que la clase 027 mide y que la 029 aprovecha para registrar el estado final.

ASP.NET Core · aspnet-core/Program.cs#

app.Use(async (contexto, siguiente) =>
{
    contexto.Response.Headers["X-Capa"] = "intermedia";
    await siguiente();
});

Igual que Express. Y un tropiezo real que quedó documentado en el propio archivo:

app.MapFallback(() => Results.Json(new { error = "no existe" }, statusCode: 404));

MapFallback y no app.Run(manejador). En ASP.NET Core, Run registra una capa terminal, que corta la tubería antes de llegar al enrutado — con ella, incluso /a y /b respondían 404.

MapFallback registra una ruta comodín, que se evalúa después de las demás. Dos métodos con nombres parecidos y comportamientos opuestos: el tipo de detalle que solo se aprende rompiéndolo.

🔬 Comparación#

Framework Modelo ¿Se puede olvidar continuar? Registro
Express cadena con siguiente() , y cuelga la petición app.use
Gin cadena con Next() motor.Use
ASP.NET Core cadena con siguiente() app.Use
Laravel cadena, clase con handle lista de capas
Rails Rack, objeto con call config.middleware.use
Spring Boot filtro con doFilter componente descubierto
Django fábrica de función lista en MIDDLEWARE
FastAPI cadena con await siguiente decorador
Fastify ganchos por fase no: no hay cadena addHook
Flask ganchos por fase no decorador

Ocho de diez usan la cadena y dos usan ganchos. La diferencia no es cosmética:

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 026

🧪 Reto de transferencia#

Añade una segunda capa que mida la duración de la petición y la emita en server-timing. Después decide dónde registrarla respecto a la primera, y justifica el orden. La clase 027 te dará el criterio.

🔗 Enlaces#

Fuentes#