Comprendiendo los Middleware en Koa.js con Async/Await

Koa.js utiliza las características de async y await de ECMAScript para gestionar operaciones asíncronas de manera elegante. La función use() en una instancia de Koa acepta middleware como argumentos.

Ejemplo Fundamental de Middleware en Koa.js

Observemos cómo se ejecutan los middleware en secuencia y cómo el control pasa de uno a otro utilizando una estructura recursiva.


// Colección de funciones middleware
const middlewareStack = [
    async (nextMiddleware) => {
        console.log('Paso 1: Inicio');
        await nextMiddleware(); // Llama al siguiente middleware
        console.log('Paso 2: Fin');
    },
    async (nextMiddleware) => {
        console.log('Paso 3: Procesando...');
        // Simulación de una operación asíncrona
        await new Promise((resolve) => {
            setTimeout(() => {
                console.log('Paso 4: Operación asíncrona completada');
                resolve();
            }, 1000);
        });
        await nextMiddleware(); // Llama al siguiente middleware
        console.log('Paso 5: Retorno de procesamiento');
    },
    async (nextMiddleware) => {
        console.log('Paso 6: Último middleware en la cadena activa');
        // No llama a nextMiddleware(), por lo que la ejecución se detiene aquí para esta rama
    },
    async (nextMiddleware) => {
        // Este middleware y los siguientes no se ejecutarán porque el middleware anterior no llamó a nextMiddleware()
        console.log('Paso 7: Inalcanzable');
        await nextMiddleware();
        console.log('Paso 8: Inalcanzable');
    }
];

/**
 * Función para procesar la pila de middleware de forma secuencial.
 * @param {Array<function>} middlewares - El array de funciones middleware.
 */
function processMiddlewares(middlewares) {
    /**
     * Función recursiva para ejecutar un middleware en un índice dado.
     * @param {number} index - El índice del middleware actual a ejecutar.
     */
    function execute(index) {
        // Si el índice está fuera de los límites, hemos terminado.
        if (index >= middlewares.length) {
            return Promise.resolve();
        }

        const currentMiddleware = middlewares[index];
        // Crea la función 'next' para este middleware, que llamará a la función 'execute' para el siguiente índice.
        const next = execute.bind(null, index + 1);

        // Ejecuta el middleware actual, pasándole el contexto y la función 'next'.
        // Usamos Promise.resolve para asegurar que el resultado sea una promesa, manejando casos donde el middleware no sea async.
        return Promise.resolve(currentMiddleware(next));
    }

    // Inicia la ejecución desde el primer middleware (índice 0).
    return execute(0);
}

// Ejecuta el pipeline de middleware
processMiddlewares(middlewareStack)
    .then(() => console.log("--- Pipeline de middleware completado ---"))
    .catch(error => console.error("Error en el pipeline:", error));

/* Salida esperada:
Paso 1: Inicio
Paso 3: Procesando...
Paso 6: Último middleware en la cadena activa
(espera 1 segundo)
Paso 4: Operación asíncrona completada
Paso 2: Fin
--- Pipeline de middleware completado ---
*/

</function>

Implementación de un Framewrok de Middleware Simplificado

Vamos a crear una estructura similar a Koa.js para manejar middleware, utilizando un enfoque basado en clases.


// Archivo: simple-koa.js
const http = require('http');
const url = require('url'); // Módulo para parsear URLs

class SimpleKoa {
    constructor() {
        this.middlewareRegistry = []; // Almacena las funciones middleware registradas
    }

    /**
     * Registra una nueva función middleware.
     * @param {Function} middlewareFn - La función middleware a registrar.
     * @returns {SimpleKoa} La instancia de SimpleKoa para encadenamiento.
     */
    use(middlewareFn) {
        this.middlewareRegistry.push(middlewareFn);
        return this; // Permite el encadenamiento de .use()
    }

    /**
     * Construye el pipeline de ejecución de middleware.
     * Devuelve una función que toma el contexto (ctx) y lo pasa a través de los middleware.
     * @param {Array<function>} registry - El registro de middleware a procesar.
     * @returns {Function} Una función que inicia la ejecución del pipeline.
     */
    buildPipeline(registry) {
        return (context) => {
            /**
             * Función recursiva interna para procesar los middleware uno por uno.
             * @param {number} index - El índice del middleware actual en el registro.
             */
            const dispatch = (index) => {
                // Si el índice excede el tamaño del registro, la cadena ha terminado.
                if (index >= registry.length) {
                    return Promise.resolve();
                }

                const currentMiddleware = registry[index];

                // Prepara la función 'next' que llamará a la función 'dispatch' para el siguiente middleware.
                const next = dispatch.bind(null, index + 1);

                // Envuelve la ejecución del middleware en una promesa para manejar correctamente async/await
                // y asegurar que 'next' se pase como argumento.
                try {
                    // Promise.resolve maneja tanto funciones asíncronas como síncronas.
                    return Promise.resolve(currentMiddleware(context, next));
                } catch (error) {
                    // Rechaza la promesa si ocurre un error sincrónico.
                    return Promise.reject(error);
                }
            };

            // Inicia la ejecución del pipeline desde el primer middleware.
            return dispatch(0);
        };
    }

    /**
     * Crea un objeto de contexto simplificado a partir de los objetos request y response de Node.js.
     * @param {http.IncomingMessage} req - El objeto request de Node.js.
     * @param {http.ServerResponse} res - El objeto response de Node.js.
     * @returns {object} El objeto de contexto.
     */
    createContext(req, res) {
        const parsedUrl = url.parse(req.url, true); // Parsea la URL y sus query parameters

        // Estructura básica del contexto. En un framework real, esto es mucho más extenso.
        return {
            request: {
                method: req.method,
                url: req.url,
                query: parsedUrl.query
            },
            response: {
                res: res // Referencia al objeto response nativo
            },
            // Puedes añadir más propiedades al contexto según sea necesario
        };
    }

    /**
     * Manejador principal para el servidor HTTP.
     * Se encarga de crear el contexto y ejecutar el pipeline de middleware para cada petición entrante.
     * @returns {Function} Una función manejadora compatible con http.createServer.
     */
    requestHandler() {
        // Construye la función que ejecutará el pipeline de middleware.
        const pipelineExecutor = this.buildPipeline(this.middlewareRegistry);

        return (req, res) => {
            // Crea el objeto de contexto para esta petición específica.
            const ctx = this.createContext(req, res);
            // Ejecuta el pipeline de middleware con el contexto.
            pipelineExecutor(ctx)
                .catch(err => {
                    console.error("Error en el middleware:", err);
                    // Envía una respuesta de error si algo sale mal en el pipeline.
                    if (!res.headersSent) {
                        res.statusCode = 500;
                        res.end('Internal Server Error');
                    }
                });
        };
    }

    /**
     * Inicia el servidor HTTP.
     * @param {...any} args - Argumentos pasados a app.listen de Node.js.
     */
    listen(...args) {
        // Crea un servidor HTTP usando el manejador de peticiones de nuestra instancia.
        const server = http.createServer(this.requestHandler());
        // Delega el inicio del servidor al módulo http nativo de Node.js.
        server.listen(...args);
    }
}

module.exports = SimpleKoa;
</function>

Prueba de la Implementación

Creemos un archivo de demostración para probar nuestro framework de middleware simplificado.


// Archivo: demo.js
const SimpleKoa = require('./simple-koa'); // Asumiendo que el archivo anterior se llama simple-koa.js

const app = new SimpleKoa();

// Middleware 1: Llama al siguiente middleware y luego registra el método y la URL.
app.use(async (ctx, next) => {
    console.log(`Request recibida: ${ctx.request.method} ${ctx.request.url}`);
    await next(); // Pasa el control al siguiente middleware.
    // Esta parte se ejecutará después de que los middleware posteriores hayan terminado.
    console.log(`Respuesta enviada para: ${ctx.request.method} ${ctx.request.url}`);
});

// Middleware 2: Maneja la respuesta final.
app.use(async (ctx) => {
    // Simula la finalización de la respuesta.
    ctx.response.res.end('¡Hola desde el servidor simplificado!');
});

const PORT = 5000;
app.listen(PORT, () => {
    console.log(`Servidor escuchando en http://localhost:${PORT}`);
});


Para ejecutar la demostración, guarda los archivos como simple-koa.js y demo.js en el mismo directorio, y luego ejecuta node demo.js en tu terminal. Al acceder a http://localhost:5000 en tu navegador, verás el mensaje "¡Hola desde el servidor simplificado!" y las consolas mostrarán el flujo de ejecución de los middleware.

Etiquetas: koa.js Middleware Node.js async await

Publicado el 7-9 04:38